package ru.yandex.webmaster3.core.semantic.semantic_document_parser.rdfa.jsonld;

import lombok.extern.slf4j.Slf4j;
import ru.yandex.common.util.Su;
import ru.yandex.common.util.URLUtils;
import ru.yandex.common.util.collections.*;
import ru.yandex.common.util.functional.*;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.location.EntityLocation;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.FrontEnd;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.rdfa.data.*;

import java.io.ByteArrayInputStream;
import java.net.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.Pattern;

import static ru.yandex.common.util.StringUtils.isEmpty;

/**
 * Created by IntelliJ IDEA.
 * User: rasifiel
 * Date: 18.02.14
 * Time: 1:31
 */
@Slf4j
public class JSONLDParser {
    private static final Set<String> CONTAINER_TYPES = Cf.set("@list", "@set", "@index", "@language");
    private static final Set<String> VALUE_PROPERTIES = Cf.set("@value", "@language", "@type", "@index");
    private ContextManager contextManager;

    public void setContextManager(final ContextManager contextManager) {
        this.contextManager = contextManager;
    }

    private FrontEnd frontEnd;

    public void setFrontEnd(final FrontEnd frontEnd) {
        this.frontEnd = frontEnd;
    }

    private boolean loadRemoteContext;

    public void setLoadRemoteContext(boolean loadRemoteContext) {
        this.loadRemoteContext = loadRemoteContext;
    }

    //   private static final Set<String> schemaOrgUrls = Cf.set("http://schema.org","https://www.schema.org","https://schema.org","http://www.schema.org");
  //  private static final String schemaOrgUniUrl = "http://schema.org";
    private static Pattern start_with_schema = Pattern.compile("http?://(www.)?schema.org.*");
  //  private static Set<String> yandexSchemaUrls = Cf.set("http://webmaster.yandex.ru","https://webmaster.yandex.ru");
   // private static String yandexSchemaUrl = "http://webmaster.yandex.ru";
    private static final Set<String> KEYWORDS =
            Cf.set("@context", "@id", "@value", "@language", "@type", "@container", "@list", "@set", "@reverse",
                    "@index", "@base", "@vocab", "@graph");

    public static boolean isKeyword(final String s) {
        return KEYWORDS.contains(s);
    }

    public List<RDFaEntity> expandDocument(final RDFaEntity entity, final String url) {
        List<String> contextUrls = getContextUrls(entity);
        // hack for for schema.json

        if (loadRemoteContext && !preloadContext(contextUrls).isEmpty()) {
            throw new JSONLDExpansionException("Loading remote context failed");
        }

        return expand(entity, null, new Context(url, null));
    }

    private List<String> getContextUrls(RDFaEntity entity) {
        List<String> contextUrls = new ArrayList<>();
        for (RDFaProperty c : entity.getProperty("@context")) {
            if (c instanceof RDFaValueProperty) {
                String value = ((RDFaValueProperty) c).getValue();
                if (value != null) {
                    contextUrls.add(((RDFaValueProperty) c).getValue());
                }
            }
        }
        for (RDFaProperty c : entity.getValuePairs()) {
            if (c instanceof RDFaComplexProperty) {
                contextUrls.addAll(getContextUrls(((RDFaComplexProperty) c).entity));
            }
        }
        return contextUrls;
    }

    public List<RDFaEntity> expand(final RDFaEntity entity, final String propName, Context context) {
        if (entity.hasProperty("@context")) {
            context = contextProcessing(context, entity.getProperty("@context"), new HashSet<String>());
        }
        final RDFaEntity result = new JSONLDEntity(null, null,entity.getLocation());
        for (final String key : entity.getPropertyList()) {
            if (key.equals("@context")) {
                continue;
            }
            final String expandedProp = iriExpand(key, context, true, false, null, null, getLocation(entity, key));
            List<RDFaProperty> expandedValue = Collections.emptyList();
            if (expandedProp != null && (expandedProp.contains(":") || isKeyword(expandedProp))) {
                if (isKeyword(expandedProp)) {
                    if ("@reverse".equals(propName)) {
                        throw new JSONLDExpansionException("Invalid reverse property map", getLocation(entity, "@reverse"));
                    }
                    if (result.hasProperty(expandedProp)) {
                        throw new JSONLDExpansionException("Colliding keywords");
                    } else if (expandedProp.equals("@id")) {
                        if (entity.getProperty(key).size() != 1) {
                            throw new JSONLDExpansionException("Invalid @id value", getLocation(entity, key));
                        }
                        RDFaProperty prop = entity.getFirstOrNull(key);
                        if (!(prop instanceof RDFaValueProperty)) {
                            throw new JSONLDExpansionException("Invalid @id value", prop.getLocation());
                        } else {
                            final String expVal =
                                    iriExpand(((RDFaValueProperty) prop).getValue(), context, false, true, null, null, prop.getLocation());
                            expandedValue = Cf.<RDFaProperty>list(new RDFaValueProperty("@id", null, expVal, "", prop.getLocation()));
                        }
                    } else if (expandedProp.equals("@type")) {
                        final List<RDFaProperty> expVals = new ArrayList<RDFaProperty>();
                        for (final RDFaProperty val : entity.getProperty(key)) {
                            if (!(val instanceof RDFaValueProperty)) {
                                throw new JSONLDExpansionException("Invalid type value", val.getLocation());
                            } else {
                                expVals.add(new RDFaValueProperty("@type", null,
                                        iriExpand(((RDFaValueProperty) val).getValue(), context, true, false, null,
                                                null, val.getLocation()), "", val.getLocation()));
                            }
                        }
                        expandedValue = expVals;
                    } else if (expandedProp.equals("@graph")) {
                        expandedValue = expand(entity.getProperty(key), key, context, expandedProp);
                    } else if (expandedProp.equals("@value")) {
                        if (entity.getProperty(key).size() != 1) {
                            throw new JSONLDExpansionException("Invalid value object value", getLocation(entity, key));
                        }
                        RDFaProperty prop = entity.getFirstOrNull(key);
                        if (!(prop instanceof RDFaValueProperty)) {
                            throw new JSONLDExpansionException("Invalid value object value", prop.getLocation());
                        } else {
                            expandedValue = Cf.list(prop);
                        }
                    } else if (expandedProp.equals("@language")) {
                        if (entity.getProperty(key).size() != 1) {
                            throw new JSONLDExpansionException("Invalid language-tagged value", getLocation(entity, key));
                        }
                        RDFaProperty prop = entity.getFirstOrNull(key);
                        if (!(prop instanceof RDFaValueProperty)) {
                            throw new JSONLDExpansionException("Invalid language-tagged value", prop.getLocation());
                        } else {
                            expandedValue = Cf.<RDFaProperty>list(new RDFaValueProperty(prop.propId,
                                    ((RDFaValueProperty) prop).textValue.toLowerCase(),
                                    ((RDFaValueProperty) prop).hrefValue, ((RDFaValueProperty) prop).htmlValue, prop.getLocation()));
                        }
                    } else if (expandedProp.equals("@index")) {
                        if (entity.getProperty(key).size() != 1) {
                            throw new JSONLDExpansionException("Invalid @index value", getLocation(entity, key));
                        }
                        RDFaProperty prop = entity.getFirstOrNull(key);
                        if (!(prop instanceof RDFaValueProperty)) {
                            throw new JSONLDExpansionException("Invalid @index value", prop.getLocation());
                        } else {
                            expandedValue = Cf.<RDFaProperty>list(new RDFaValueProperty(prop.propId,
                                    ((RDFaValueProperty) prop).textValue.toLowerCase(),
                                    ((RDFaValueProperty) prop).hrefValue, ((RDFaValueProperty) prop).htmlValue, prop.getLocation()));
                        }
                    } else if (expandedProp.equals("@list")) {
                        if (propName == null || propName.equals("@graph")) {
                            continue;
                        }
                        expandedValue = expand(entity.getProperty(key), key, context, expandedProp);
                        /*if (expandedValue.size() > 1) {
                            throw new JSONLDExpansionException("List of lists");
                        }*/
                    } else if (expandedProp.equals("@set")) {
                        expandedValue = expand(entity.getProperty(key), key, context, expandedProp);
                    } else if (expandedProp.equals("@reverse")) {
                        RDFaProperty e = entity.getFirstOrNull(key);
                        if (e instanceof RDFaComplexProperty) {
                            List<RDFaEntity> values = expand(((RDFaComplexProperty) e).entity, "@reverse", context);
                            if (values.size() != 1) {
                                throw new JSONLDExpansionException("invalid reverse property", e.getLocation());
                            }
                            RDFaEntity reverseValue = values.get(0);
                            if (reverseValue.hasProperty("@reverse")) {
                                RDFaProperty doubleReverse = reverseValue.getFirstOrNull("@reverse");
                                if (doubleReverse instanceof RDFaComplexProperty) {
                                    result.appendProperties(
                                            ((RDFaComplexProperty) doubleReverse).entity.getValuePairs());
                                } else {
                                    throw new JSONLDExpansionException("invalid reverse property", doubleReverse.getLocation());
                                }
                            }
                            RDFaEntity reverseMap = new JSONLDEntity(null, null,e.getLocation());
                            for (final RDFaProperty prop : reverseValue.getValuePairs()) {
                                if (prop instanceof RDFaComplexProperty) {
                                    if (((RDFaComplexProperty) prop).entity.hasProperty("@value") ||
                                            ((RDFaComplexProperty) prop).entity.hasProperty("@list")) {
                                        throw new JSONLDExpansionException("invalid reverse property",prop.getLocation());
                                    }
                                }
                                reverseMap.appendProperty(prop);
                            }
                            if (!reverseMap.isEmpty()) {
                                result.appendProperty(new RDFaComplexProperty("@reverse", reverseMap,e.getLocation()));
                            }
                        }
                    }
                    for (final RDFaProperty property : expandedValue) {
                        result.appendProperty(property);
                    }
                } else {
                    TermDefinition definition = context.getDefinition(key);
                    if (definition != null && "@language".equals(definition.container) &&
                            entity.getProperty(key).size() == 1 &&
                            entity.getFirstOrNull(key) instanceof RDFaComplexProperty) {
                        expandedValue = Cf.newArrayList();
                        RDFaProperty prop = entity.getFirstOrNull(key);
                        if (prop instanceof RDFaComplexProperty) {
                            final RDFaEntity vals = ((RDFaComplexProperty) prop).entity;
                            vals.setLocation(prop.getLocation());
                            for (final String language : vals.getPropertyList()) {
                                final List<String> langVals = Cu.map(new Function<RDFaProperty, String>() {
                                    @Override
                                    public String apply(final RDFaProperty rdFaProperty) {
                                        if (rdFaProperty instanceof RDFaValueProperty) {
                                            return ((RDFaValueProperty) rdFaProperty).getValue();
                                        }
                                        throw new JSONLDExpansionException("Invalid language map value",rdFaProperty.getLocation());
                                    }
                                }, vals.getProperty(language));
                                expandedValue.addAll(Cu.map(new Function<String, RDFaProperty>() {
                                    @Override
                                    public RDFaProperty apply(final String s) {
                                        RDFaEntity result = new JSONLDEntity(null, null);
                                        result.appendProperty(new RDFaValueProperty("@value", s, null, ""));
                                        result.appendProperty(
                                                new RDFaValueProperty("@language", language.toLowerCase(), null, "", vals.getLocation()));
                                        return new RDFaComplexProperty(key, result,vals.getLocation());
                                    }
                                }, langVals));
                            }
                        }
                    } else if (definition != null && "@index".equals(definition.container) &&
                            entity.getProperty(key).size() == 1 &&
                            entity.getFirstOrNull(key) instanceof RDFaComplexProperty) {
                        RDFaProperty prop = entity.getFirstOrNull(key);
                        if (prop instanceof RDFaComplexProperty) {
                            RDFaEntity vals = ((RDFaComplexProperty) prop).entity;
                            expandedValue = new ArrayList<RDFaProperty>();
                            for (final String index : vals.getPropertyList()) {
                                List<RDFaProperty> res = expand(vals.getProperty(index), key, context, expandedProp);
                                for (RDFaProperty val : res) {
                                    if (val instanceof RDFaComplexProperty) {
                                        final RDFaEntity ent = ((RDFaComplexProperty) val).entity;
                                        if (!ent.hasProperty("@index")) {
                                            ent.appendProperty(new RDFaValueProperty("@index", index, null, "", ent.getLocation()));
                                        }
                                        expandedValue.add(val);
                                    }
                                }
                            }
                        }
                    } else {
                        expandedValue = expand(entity.getProperty(key), key, context, expandedProp);
                    }
                    if (expandedValue.isEmpty()) {
                        continue;
                    }
                    if (definition != null && "@list".equals(definition.container)) {
                        final RDFaEntity res = new JSONLDEntity(null, null);
                        res.appendProperties(expandedValue);
                        expandedValue = Cf.<RDFaProperty>list(new RDFaComplexProperty(expandedProp, res));
                    }
                    if (definition != null && definition.isReverse) {
                        RDFaProperty reverseMapProp = result.getFirstOrNull("@reverse");
                        RDFaEntity reverseMap = null;
                        if (reverseMapProp != null && reverseMapProp instanceof RDFaComplexProperty) {
                            reverseMap = ((RDFaComplexProperty) reverseMapProp).entity;
                        }
                        if (reverseMap == null) {
                            reverseMap = new JSONLDEntity(null, null);
                            result.addProperty(new RDFaComplexProperty("@reverse", reverseMap));
                        }
                        for (RDFaProperty x : expandedValue) {
                            reverseMap.appendProperty(x);
                        }
                    } else {
                        result.appendProperties(expandedValue);
                    }
                }
            }
        }
        if (result.hasProperty("@value")) {
            for (String prop : result.getPropertyList()) {
                if (!VALUE_PROPERTIES.contains(prop)) {
                    throw new JSONLDExpansionException("Invalid value object value", result.getLocation());
                }
            }
            if (result.hasProperty("@language") && result.hasProperty("@type")) {
                throw new JSONLDExpansionException("Invalid value object value", result.getLocation());
            }
            List<RDFaProperty> props = Cu.filterAsList(result.getProperty("@value"), new Filter<RDFaProperty>() {
                @Override
                public boolean fits(final RDFaProperty property) {
                    if (property instanceof RDFaValueProperty) {
                        if (((RDFaValueProperty) property).getValue() == null) {
                            return false;
                        }
                    }
                    return true;
                }
            });
            if (props.isEmpty()) {
                return Collections.EMPTY_LIST;
            }
            result.filterEntity(new Filter<RDFaProperty>() {
                @Override
                public boolean fits(final RDFaProperty property) {
                    return !property.propId.equals("@value");
                }
            });
            result.appendProperties(props);
            if (result.hasProperty("@language")) {
                getStringOrException(result, "@value", "invalid language-tagged value");
            }
            if (result.hasProperty("@type")) {
                String type = getStringOrException(result, "@type", "invalid typed value");
                if (isEmpty(type) || !URLUtils.isValidHttpURL(type)) {
                    throw new JSONLDExpansionException("invalid typed value", result.getLocation());
                }
            }
        }
        if (result.hasProperty("@set") || result.hasProperty("@list")) {
            if (result.getPropertyList().size() > 2 ||
                    (result.getPropertyList().size() == 2 && !result.hasProperty("@index"))) {
                throw new JSONLDExpansionException("invalid set or list object", result.getLocation());
            }
            if (result.hasProperty("@set")) {
                List<RDFaProperty> r = result.getProperty("@set");
                List<RDFaEntity> resultSet = Cu.mapWhereDefined(new PartialFunction<RDFaProperty, RDFaEntity>() {
                    @Override
                    public RDFaEntity apply(final RDFaProperty arg) throws IllegalArgumentException {
                        if (arg instanceof RDFaComplexProperty) {
                            return ((RDFaComplexProperty) arg).entity;
                        }
                        throw new IllegalArgumentException();
                    }
                }, r);
                return Cu.filterAsList(resultSet, new Filter<RDFaEntity>() {
                    @Override
                    public boolean fits(final RDFaEntity result) {
                        if (propName == null || "@graph".equals(propName)) {
                            if (result.isEmpty() || result.hasProperty("@value") || result.hasProperty("@list")) {
                                return false;
                            } else if (result.hasProperty("@id") && result.getPropertyList().size() == 1) {
                                return false;
                            }
                        }
                        return true;
                    }
                });
            }
        }
        if (result.hasProperty("@language") && result.getPropertyList().size() == 1) {
            return Collections.EMPTY_LIST;
        }
        if (propName == null || "@graph".equals(propName)) {
            if (result.isEmpty() || result.hasProperty("@value") || result.hasProperty("@list")) {
                return Collections.EMPTY_LIST;
            } else if (result.hasProperty("@id") && result.getPropertyList().size() == 1) {
                return Collections.EMPTY_LIST;
            }
        }
        if (result.hasProperty("@graph") && result.getPropertyList().size() == 1) {
            List<RDFaProperty> graphEl = result.getProperty("@graph");
            final List<RDFaEntity> resultGraph = new ArrayList<RDFaEntity>();
            for (RDFaProperty el : graphEl) {
                if (el instanceof RDFaComplexProperty) {
                    resultGraph.add(((RDFaComplexProperty) el).entity);
                }
            }
            return resultGraph;
        }
        if (result.isEmpty()) {
            return Collections.EMPTY_LIST;
        }
        return Cf.list(result);
    }

    private EntityLocation getLocation(RDFaEntity entity, String key) {
        RDFaProperty property = entity.getFirstOrNull(key);
        return property == null ? entity.getLocation() : property.getLocation();
    }

    private List<RDFaProperty> expand(final List<RDFaProperty> propVals, final String key, final Context context, final String expandedProp) {
        TermDefinition termDef = context.getDefinition(key);
        final List<RDFaProperty> result = new ArrayList<RDFaProperty>(propVals.size());
        for (final RDFaProperty prop : propVals) {
            if (prop instanceof RDFaValueProperty) {
                {
                    EntityLocation location = prop.getLocation();
                    if (((RDFaValueProperty) prop).getValue() == null) {
                        continue;
                    }
                    if ("@graph".equals(key)) {
                        continue;
                    }
                    if (termDef != null && "@id".equals(termDef.type)) {

                        RDFaValueProperty valProp;
                            try {
                                valProp = new RDFaValueProperty("@id", null,
                                        iriExpand(((RDFaValueProperty) prop).getValue(), context, false, true, null, null, location),
                                        "", location);
                                final RDFaEntity entity = new JSONLDEntity(null, null,location);
                                entity.appendProperty(valProp);
                                result.add(new RDFaComplexProperty(expandedProp, entity));
                            } catch (JSONLDExpansionResolveUriArgumentException e) {
                                final RDFaEntity entity = new JSONLDEntity(null, null,location);
                                entity.appendProperty(new RDFaValueProperty("@value", ((RDFaValueProperty) prop).textValue,
                                        ((RDFaValueProperty) prop).hrefValue, "",location));
                                if (termDef != null && termDef.type != null && !("@id").equals(termDef.type)) {
                                    entity.appendProperty(new RDFaValueProperty("@type", termDef.type, null, "",location));
                                } else if (termDef != null && termDef.language != null) {
                                    entity.appendProperty(new RDFaValueProperty("@language", termDef.language, null, "", location));
                                } else if (context.defaultLanguage != null) {
                                    entity.appendProperty(
                                            new RDFaValueProperty("@language", context.defaultLanguage, null, "",location));
                                }
                                result.add(new RDFaComplexProperty(expandedProp, entity));
                            }

                    } else if (termDef != null && "@vocab".equals(termDef.type)) {
                        RDFaValueProperty valProp = new RDFaValueProperty("@id", null,
                                iriExpand(((RDFaValueProperty) prop).getValue(), context, true, true, null, null, location), "", location);
                        final RDFaEntity entity = new JSONLDEntity(null, null,location);
                        entity.appendProperty(valProp);
                        result.add(new RDFaComplexProperty(expandedProp, entity));
                    } else {
                        final RDFaEntity entity = new JSONLDEntity(null, null,location);
                        entity.appendProperty(new RDFaValueProperty("@value", ((RDFaValueProperty) prop).textValue,
                                ((RDFaValueProperty) prop).hrefValue, "",location));
                        if (termDef != null && termDef.type != null) {
                            entity.appendProperty(new RDFaValueProperty("@type", termDef.type, null, "",location));
                        } else if (termDef != null && termDef.language != null) {
                            entity.appendProperty(new RDFaValueProperty("@language", termDef.language, null, "",location));
                        } else if (context.defaultLanguage != null) {
                            entity.appendProperty(
                                    new RDFaValueProperty("@language", context.defaultLanguage, null, "",location));
                        }
                        result.add(new RDFaComplexProperty(expandedProp, entity));
                    }
                }
            } else {
                RDFaEntity entity = ((RDFaComplexProperty) prop).entity;

                for (final RDFaEntity e : expand(entity, prop.propId, context)) {
                    result.add(new RDFaComplexProperty(expandedProp, e));
                }
            }
        }
        return result;
    }

    public String iriExpand(final String value, final Context context, final boolean vocab, final boolean documentRelative,
                            final RDFaEntity localContext, final Map<String, Boolean> defined, EntityLocation location)
    {
        if (value == null || isKeyword(value)) {
            return value;
        }
        if (localContext != null) {
            if (localContext.hasProperty(value) && defined != null) {
                if (!defined.containsKey(value) || !defined.get(value)) {
                    createTermDef(context, localContext, value, defined);
                }
            }
        }
        if (vocab && context.containsTerm(value)) {
            TermDefinition termDefinition = context.getDefinition(value);
            return termDefinition.iri;
        }
        if (value.contains(":")) {
            String[] parts = Su.split(value, ':', 2);
            if (parts[0].equals("_") || parts[1].startsWith("//")) {
                return value;
            }
            String prefix = parts[0];
            String suffix = parts[1];
            if (localContext != null && localContext.hasProperty(prefix) && defined != null &&
                    !defined.containsKey(prefix)) {
                createTermDef(context, localContext, prefix, defined);
            }
            if (context.containsTerm(prefix)) {
                TermDefinition termDefinition = context.getDefinition(prefix);
                if (termDefinition.iri != null) {
                    return termDefinition.iri + suffix;
                }
            }
            return value;
        }
        if (vocab && context.vocabMapping != null) {
            return context.vocabMapping + value;
        }
        if (documentRelative) {
            return resolve(context, value, location);
        }
        return value;
    }

    private String resolve(final Context context, final String value, EntityLocation location) {
        try {
            if (isEmpty(context.baseUri)||("http://localhost".equals(context.baseUri))) {
                try {
                    URI uri = new URI(value);
//                    if(uri.getHost()==null){
//                       throw new URISyntaxException("Empty Host","Whole Url expected");
//                    }
//                    else {
                        return uri.toString();
//                    }
                } catch (URISyntaxException e) {
                    throw new IllegalArgumentException();
                }
            }
            if (value == null) {
                throw new JSONLDExpansionException("invalid base IRI", location);
            }

            URI baseUri = new URI(context.baseUri);
            return baseUri.resolve(value).toString();

        } catch (URISyntaxException e) {
            throw new JSONLDExpansionException("invalid base IRI", location);

        } catch (IllegalArgumentException e) {
            throw new JSONLDExpansionResolveUriArgumentException("invalid base IRI", location);
        }
    }


    private void createTermDef(final Context context, final RDFaEntity localContext, final String term, final Map<String, Boolean> defined) {
        if (defined.containsKey(term) && defined.get(term)) {
            return;
        }
        if (defined.containsKey(term) && !defined.get(term)) {
            throw new JSONLDExpansionException("Cyclic IRI mapping", localContext.getLocation());
        }
        defined.put(term, false);
        if (isKeyword(term)) {
            throw new JSONLDExpansionException("Keyword redefinition", getLocation(localContext, term));
        }
        context.removeDefinition(term);
        if (localContext.getProperty(term).size() > 1) {
            throw new JSONLDExpansionException("Invalid term definition", getLocation(localContext, term));
        }
        RDFaProperty def = localContext.getFirstOrNull(term);
        if (def == null) {
            defined.put(term, true);
            context.addDefinition(term, null);
        }
        RDFaEntity convVal = new JSONLDEntity(null, null);
        if (def instanceof RDFaValueProperty) {
            convVal.appendProperty(new RDFaValueProperty("@id", null, ((RDFaValueProperty) def).getValue(), "", def.getLocation()));
        } else {
            convVal = ((RDFaComplexProperty) def).entity;
        }
        String type = null;
        if (convVal.hasProperty("@type")) {
            type = getStringOrException(convVal, "@type", "invalid type mapping");
            type = iriExpand(type, context, true, false, localContext, defined, getLocation(convVal, "@type"));
        }
        String iri = null;
        if (convVal.hasProperty("@reverse")) {
            if (convVal.hasProperty("@id")) {
                throw new JSONLDExpansionException("invalid reverse property", getLocation(convVal, "@id"));
            }
            String reverse = getStringOrException(convVal, "@reverse", "invalid IRI mapping");
            iri = iriExpand(reverse, context, true, false, localContext, defined, getLocation(convVal, "@reverse"));
            String container = null;
            if (convVal.hasProperty("@container")) {
                container = getStringOrException(convVal, "@container", "invalid reverse property");
                if (container != null && !"@set".equals(container) && !"@index".equals(container)) {
                    throw new JSONLDExpansionException("invalid reverse property", getLocation(convVal, "@container"));
                }
            }
            TermDefinition definition = new TermDefinition(iri, type, null, container, true);
            context.addDefinition(term, definition);
            return;
        }
        String id_val = null;
        try {
            id_val = getStringOrException(convVal, "@id", "");
        } catch (JSONLDExpansionException e) {
        }
        if (convVal.hasProperty("@id") && (term == null || !term.equals(id_val))) {
            iri = getStringOrException(convVal, "@id", "invalid IRI mapping");
            iri = iriExpand(iri, context, true, false, localContext, defined, getLocation(convVal, "@id"));
            if (!isKeyword(iri) && iri != null && !iri.contains(":")) {
                throw new JSONLDExpansionException("invalid IRI mapping", getLocation(convVal, "@id"));
            }
        } else if (term.contains(":")) {
            String[] parts = Su.split(term, ':', 2);
            if (!parts[0].equals("_") && !parts[1].startsWith("//")) {
                String prefix = parts[0];
                String suffix = parts[1];
                if (localContext.hasProperty(prefix)) {
                    createTermDef(context, localContext, prefix, defined);
                }
                if (context.containsTerm(prefix)) {
                    iri = context.getDefinition(prefix).iri + suffix;
                }
            } else {
                iri = term;
            }
        } else if (context.vocabMapping != null) {
            iri = context.vocabMapping + term;
        } else {
            throw new JSONLDExpansionException("invalid IRI mapping", convVal.getLocation());
        }
        String language = null;
        if (convVal.hasProperty("@language") && !convVal.hasProperty("@type")) {
            language = getStringOrException(convVal, "@language", "invalid language mapping");
            if (language != null) {
                language = language.toLowerCase();
            }
        }
        String container = null;
        if (convVal.hasProperty("@container")) {
            container = getStringOrException(convVal, "@container", "invalid container mapping");
            if (!CONTAINER_TYPES.contains(container)) {
                throw new JSONLDExpansionException("invalid container mapping", convVal.getLocation());
            }
        }
        TermDefinition newTerm = new TermDefinition(iri, type, language, container, false);
        context.addDefinition(term, newTerm);
        defined.put(term, true);
    }

    private String getStringOrException(final RDFaEntity entity, final String key, final String exception) {
        if (!entity.hasProperty(key)) {
            return null;
        }
        if (entity.getProperty(key).size() > 1) {
            throw new JSONLDExpansionException(exception, entity.getLocation());
        }
        RDFaProperty prop = entity.getFirstOrNull(key);
        if (prop instanceof RDFaValueProperty) {
            return ((RDFaValueProperty) prop).getValue();
        }
        throw new JSONLDExpansionException(exception, entity.getLocation());
    }

    public Context contextProcessing(final Context activeContext, final List<RDFaProperty> localContext, final Set<String> remoteContexts) {
        Context result = new Context(activeContext);
        for (final RDFaProperty prop : localContext) {
            if (prop instanceof RDFaValueProperty) {
                String uri = ((RDFaValueProperty) prop).getValue();
                if (uri == null) {
                    return new Context(activeContext.baseUri, null);
                }
                uri = resolve(activeContext, uri, prop.getLocation());
                if (remoteContexts.contains(uri)) {
                    throw new JSONLDExpansionException("Recursive context inclusion", prop.getLocation());
                }
                remoteContexts.add(uri);
                List<RDFaProperty> rContext = loadContext(uri);
                if (loadRemoteContext && rContext == null) {
                    throw new JSONLDExpansionException("Loading remote context failed", prop.getLocation());
                } else if (rContext == null) {
                    rContext = Collections.emptyList();
                }
                result = contextProcessing(result, rContext, remoteContexts);
            } else {
                RDFaEntity contextEntity = ((RDFaComplexProperty) prop).entity;
                if (contextEntity.hasProperty("@base") && remoteContexts.isEmpty()) {
                    String uri = getStringOrException(contextEntity, "@base", "invalid base IRI");
                    if (!isEmpty(uri) && !URLUtils.isValidHttpURL(uri)) {
                        throw new JSONLDExpansionException("invalid base IRI", contextEntity.getLocation());
                    }
                    if (uri != null) {
                        uri = resolve(result, uri, contextEntity.getLocation());
                    }
                    result.baseUri = uri;
                }
                if (contextEntity.hasProperty("@vocab")) {
                    String uri = getStringOrException(contextEntity, "@vocab", "invalid vocab mapping");
                    if (!isEmpty(uri) && !uri.startsWith("_:") && !URLUtils.isValidHttpURL(uri)) {
                        throw new JSONLDExpansionException("invalid vocab mapping", contextEntity.getLocation());
                    }
                    result.vocabMapping = uri;
                }
                if (contextEntity.hasProperty("@language")) {
                    String language = getStringOrException(contextEntity, "@language", "invalid default language");
                    if (null == language || "null".equals(language)) {
                        result.defaultLanguage = null;
                    } else {
                        result.defaultLanguage = language.toLowerCase();
                    }
                }
                final Map<String, Boolean> defined = new HashMap<String, Boolean>();
                for (String propName : contextEntity.getPropertyList()) {
                    if (!propName.equals("@base") && !propName.equals("@vocab") && !propName.equals("@language")) {
                        createTermDef(result, contextEntity, propName, defined);
                    }
                }
            }
        }
        return result;
    }

    private List<RDFaProperty> loadContext(final String uri) {
        if (contextManager == null) {
            return null;
        }
        return contextManager.getContext(uri);
    }


    /**
     * download external contexts ans store them in contextManager
     *
     * @param uris - URI list to download
     * @return list of failed URI
     */
    private List<String> preloadContext(final List<String> uris) {
        final Iterable<String> newUri = Cu.filter(uris, new Filter<String>() {
            @Override
            public boolean fits(final String s) {
                return !(contextManager.getContext(s) != null);
            }
        });
        if (uris.size() == 0) {
            return Collections.EMPTY_LIST;
        }
        log.debug("Loading contexts: " + Su.join(uris, " "));
        log.debug("New contexts: " + Su.join(newUri, " "));
        ThreadPoolExecutor executor = new ThreadPoolExecutor(uris.size(), uris.size(), 0, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(uris.size()));
        final ArrayBlockingQueue<String> result = new ArrayBlockingQueue<String>(uris.size());
        for (final String uri : newUri) {
            executor.execute(() -> {
                try {
                    Pair<byte[], Charset> r = frontEnd.downloadRaw(new URL(uri));
                    contextManager.loadContext(uri, new ByteArrayInputStream(r.first), r.second);
                } catch (JSONLDExpansionException e) {
                    throw e;
                } catch (Exception e) {
                    result.add(uri);
                }
            });
        }
        try {
            executor.shutdown();
            executor.awaitTermination(FrontEnd.CONNECT_TIMEOUT + FrontEnd.LOAD_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
        }
        return Cf.list(result);
    }

}
