package ru.yandex.webmaster3.core.semantic.semantic_document_parser.microdata.validators;

import org.springframework.beans.factory.annotation.Required;
import ru.yandex.common.util.Su;
import ru.yandex.common.util.URLUtils;
import ru.yandex.common.util.collections.Cf;
import ru.yandex.common.util.collections.Cu;
import ru.yandex.webmaster3.core.semantic.schema_org_information_extractor.SchemaClass;
import ru.yandex.webmaster3.core.semantic.schema_org_information_extractor.SchemaField;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.location.EntityLocation;
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.data.TextMicrodata;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microdata.exceptions.*;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.validators.ISO8601Validator;

import java.util.*;
import java.util.regex.Pattern;

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

/**
 * Created by IntelliJ IDEA.
 * User: rasifiel
 * Date: 4/10/12
 * Time: 2:43 AM
 */
public class SchemaValidator implements MicrodataValidator {

    private String vocab;

    @Required
    public void setVocab(final String vocab) {
        this.vocab = vocab;
    }

    private final Map<String, SchemaClass> classMaps;

    public SchemaValidator(final Map<String, SchemaClass> classMap) {
        this.classMaps = classMap;
    }

    @Override
    public List<MicrodataValidatorException> validate(final Microdata microdata) {
        if (microdata instanceof ComplexMicrodata && !isEmpty(((ComplexMicrodata) microdata).getType())) {
            final ComplexMicrodata complexMicrodata = (ComplexMicrodata) microdata;
            String[] types = complexMicrodata.getType().split(" ");
            final String hrefType = types[0];
            final String vocabHost = MicrodataUtils.getVocabHost(hrefType);
            String httpType = MicrodataUtils.addPrefixIfEmpty(hrefType);
            String vocabType = MicrodataUtils.addPrefixIfEmpty(vocab);
            List<String> vocabTypes = MicrodataUtils.protocolAliases(vocabType);
            boolean rightVocab = false;
            for(String vocab : vocabTypes){
                if(httpType.startsWith(vocab)){
                    rightVocab = true;
                }
            }
            if (rightVocab) {
                final Map<String, SchemaClass> classMap = classMaps;
                final List<MicrodataValidatorException> exceptions = new LinkedList<MicrodataValidatorException>();
                final SchemaClass cls = classMap.get(hrefType);
                if (isRole(cls)) {
                    return Collections.emptyList();
                }
                final List<String> additionalTypes =
                        MicrodataUtils.extractStringList(complexMicrodata, "additionalType");
                for (int i = 1; i < types.length; i++) {
                    additionalTypes.add(types[i]);
                }
                final List<SchemaClass> additionalClasses = new LinkedList<SchemaClass>();
                for (final String addType : additionalTypes) {
                    if (classMaps.containsKey(addType)) {
                        final SchemaClass addCls = classMap.get(addType);
                        if (addCls != null) {
                            additionalClasses.add(addCls);
                        }
                    }
                }
                if (cls == null) {
                    final String fixedType = MicrodataUtils.addPrefixIfEmpty(hrefType);
                    boolean found = false;
                    for (final String klass : classMap.keySet()) {
                        if (fixedType.toLowerCase().equals(klass.toLowerCase())) {
                            exceptions.add(new SchemaMisspellMicrodataValidatorException("type", true,
                                    hrefType + "$$" + klass + "$$" + vocab, microdata));
                            found = true;
                        }
                    }
                    if (!found) {
                        exceptions.add(
                                new NoSuchSchemaOrgClassMicrodataValidatorException(false, hrefType + "$$" + vocabHost,
                                        microdata));
                    }
                } else {
                    additionalClasses.add(cls);
                }
                if (additionalClasses.isEmpty()) {
                    return exceptions;
                }
                validate(complexMicrodata, hrefType, classMap, exceptions, additionalClasses);
                return exceptions;
            } else {
                return Collections.emptyList();
            }
        } else {
            return Collections.emptyList();
        }
    }

    private final static Set<String> ALLOWED_BOOL =
            Cf.set("true", "false", "True", "False", "http://schema.org/True", "http://schema.org/False","https://schema.org/True", "https://schema.org/False",
                    "http://www.schema.org/True", "http://www.schema.org/False","https://www.schema.org/True", "https://www.schema.org/False");

    private static Pattern start_with_schema = Pattern.compile("http?://(www.)?schema.org.*");
    private void validate(ComplexMicrodata complexMicrodata, String hrefType, Map<String, SchemaClass> classMap, List<MicrodataValidatorException> exceptions, List<SchemaClass> types) {
        for (final String fieldNameRaw : complexMicrodata.getPropList()) {
            final String fieldName;
            final String suffix;
            if (start_with_schema.matcher(hrefType).matches() ) {
                String[] parts = Su.split(fieldNameRaw, '-', 2);
                fieldName = parts[0];
                if (parts.length > 1) {
                    suffix = parts[1];
                } else {
                    suffix = "";
                }
            } else {
                fieldName = fieldNameRaw;
                suffix = "";
            }
            final List<Microdata> values = complexMicrodata.getPropAsList(fieldNameRaw);
            if ("itemId".equals(fieldNameRaw)) {
                continue;
            }
            if (URLUtils.isValidHttpURL(fieldNameRaw)) {
                continue;
            }
            boolean foundField = false;
            final List<SchemaField> fields = new LinkedList<SchemaField>();
            for (final SchemaClass curCls : types) {
                String longFieldName =
                        curCls.name.substring(0, curCls.name.length() - curCls.label.length()) + fieldName;


                final SchemaField fld = curCls.fields.get(longFieldName);
                if (fld == null) {
                    continue;
                }
                foundField = true;
                fields.add(fld);
            }
            if (!foundField) {

                EntityLocation fieldLocation;
                try {
                    fieldLocation =
                            values.get(0).location;
                }
                catch (Exception e){
                    fieldLocation = complexMicrodata.location;
                }
                SchemaClass foundScheme = null;
                String foundFieldName = null;
                for (final SchemaClass curCls : types) {
                    // field name to long
                    for (final String field : curCls.fields.keySet()) {

                        String longFieldName =
                                curCls.name.substring(0, curCls.name.length() - curCls.label.length()) + fieldName;

                        if (field.toLowerCase().equals(longFieldName.toLowerCase())) {
                            foundScheme = curCls;
                            foundFieldName = field;
                            foundField = true;
                            break;
                        }
                    }
                    if (foundField) {
                        break;
                    }
                }
                if (foundField) {
                    List<String> ffn = Arrays.asList(foundFieldName.split("/"));
                    foundFieldName = ffn.get(ffn.size() - 1);
                    exceptions.add(new SchemaMisspellMicrodataValidatorException("field", true,
                            fieldName + "$$" + foundScheme.name + "$$" + foundFieldName, complexMicrodata, fieldLocation));
                } else {
                    exceptions.add(
                            new NoSuchSchemaOrgFieldMicrodataValidatorException(false, fieldName + "$$" + hrefType,
                                    complexMicrodata,fieldLocation));
                }
            } else {
                for (final Microdata value : values) {
                    EntityLocation valueLocation = value.location;
                    if (value instanceof TextMicrodata) {


                        List<MicrodataValidatorException> tempExceptions = new ArrayList<MicrodataValidatorException>();
                        boolean knownType = false;

                        if (onlyType(fields, "http://schema.org/Date")) {
                            if (!ISO8601Validator.validate(((TextMicrodata) value).data)) {
                                tempExceptions.add(new DatetimeFormatMicrodataValidatorException(false,
                                        ((TextMicrodata) value).data + "$$" + fieldName, complexMicrodata, valueLocation));
                            }
                            else{
                                knownType = true;
                            }
                        } else if (onlyType(fields, "http://schema.org/Number")) {
                            if (!FLOAT_REGEXP.matcher(((TextMicrodata) value).data).matches()) {
                                tempExceptions.add(new SchemaTypeMicrodataValidatorException("number", false,
                                        ((TextMicrodata) value).data + "$$" + fieldName, complexMicrodata,valueLocation));
                            }
                            else{
                                knownType = true;
                            }
                        } else if (onlyType(fields, "http://www.w3.org/2001/XMLSchema#float")) {
                            if (!FLOAT_REGEXP.matcher(((TextMicrodata) value).data).matches()) {
                                tempExceptions.add(new SchemaTypeMicrodataValidatorException("float", false,
                                        ((TextMicrodata) value).data + "$$" + fieldName, complexMicrodata,valueLocation));
                            }
                            else{
                                knownType = true;
                            }
                        } else if (onlyType(fields, "http://schema.org/Integer")) {
                            String textValue = ((TextMicrodata) value).data;
                            if(fieldName.matches("fileSize")){
                                if(!FILESIZE_REGEXP.matcher(textValue).matches()){
                                    tempExceptions.add(
                                            new SchemaTypeMicrodataValidatorException("integer", false, textValue + "$$" + fieldName, complexMicrodata,valueLocation));
                                }
                            }
                            else if (!INTEGER_REGEXP.matcher(((TextMicrodata) value).data).matches()) {
                                tempExceptions.add(new SchemaTypeMicrodataValidatorException("integer", false,
                                        ((TextMicrodata) value).data + "$$" + fieldName, complexMicrodata,valueLocation));
                            }
                            else{
                                knownType = true;
                            }
                        } else if (onlyType(fields, "http://schema.org/Boolean")) {
                            if (!ALLOWED_BOOL.contains(((TextMicrodata) value).data)) {
                                tempExceptions.add(new SchemaTypeMicrodataValidatorException("boolean", false,
                                        ((TextMicrodata) value).data + "$$" + fieldName, complexMicrodata,valueLocation));
                            } else {
                                knownType = true;
                            }
                        } else if (onlyType(fields, "http://schema.org/URL")) {
                            if (((TextMicrodata) value).html.matches("^\\s*<\\s*meta.*")){
                                if (!((TextMicrodata) value).data.isEmpty()) {
                                    tempExceptions.add(new MicrodataValidatorException(true,"",complexMicrodata,valueLocation) {
                                        @Override
                                        public String getKey() {
                                            return "meta_content_url";
                                        }
                                    });
                                }
                            }
                            else
                            if (((TextMicrodata) value).getHref().isEmpty()) {
                                tempExceptions.add(new SchemaTypeMicrodataValidatorException("url", false,
                                        ((TextMicrodata) value).getHref() + "$$" + fieldName, complexMicrodata, valueLocation));
                            }
                            else{
                                knownType = true;
                            }
                        }
                        if(!knownType){
                            exceptions.addAll(tempExceptions);
                        }
                        if(fieldName.equals("priceCurrency")){
                            if(!ISO4217Validatior.validate(((TextMicrodata) value).data)){
                                exceptions.add(new SchemaTypeMicrodataValidatorException("currency", false,
                                        ((TextMicrodata) value).data + "$$" + fieldName, complexMicrodata, valueLocation));
                            }
                        }
                        // text value ok for all fields
                    } else if (value instanceof ComplexMicrodata) {
                        boolean foundType = false;
                        ArrayList<String> validTypes = new ArrayList<String>();
                        SchemaClass chkCls = classMap.get(((ComplexMicrodata) value).getType());
                        if (isRole(chkCls)) {
                            validate((ComplexMicrodata) value, ((ComplexMicrodata) value).getType(), classMap,
                                    exceptions, Cu.join(Cf.list(chkCls), types));
                        } else {
                            for (final SchemaField fld : fields) {
                                if ((fld.ranges.size() == 1) && (fld.ranges.get(0).endsWith("#"))) {

                                    String range = fld.ranges.get(0);

                                    final SchemaClass schemaClass = classMap.get(range);
                                    validate((ComplexMicrodata) value, ((ComplexMicrodata) value).getType(), classMap,
                                            exceptions, Cf.list(schemaClass));

                                }
                                List<String> ranges = fld.ranges;
                                if ("input".equals(suffix) || "output".equals(suffix)) {
                                    ranges = Cu.join(ranges, Cf.list("http://schema.org/PropertyValueSpecification","https://schema.org/PropertyValueSpecification"));
                                }

                                if (chkCls != null && checkClass(chkCls, ranges)) {
                                    foundType = true;
                                } else {
                                    for (String rng : ranges) {
                                        validTypes.add(rng);
                                    }
                                }

                            }
                            if (!foundType) {
                                exceptions.add(new WrongClassForFieldMicrodataValidatorException(false,
                                        fieldName + "$$" + ((ComplexMicrodata) value).getType() + "$$" +
                                                Su.join(MicrodataUtils.httpPrefixOnly(validTypes), ", "), complexMicrodata,valueLocation));
                            }
                        }
                    }
                }
            }
        }
    }


    private List<SchemaClass> makeAllowed(final Collection<String> ranges, final Map<String, SchemaClass> classMap, final SchemaClass cls) {
        return Cu.join(Cu.project(classMap, ranges).values(), Cf.list(cls));
    }

    private boolean isRole(final SchemaClass cls) {
        return cls!=null && cls.name!=null && start_with_schema.matcher(cls.name).matches() && cls.name.endsWith("Role");
    }

    private final static Pattern FLOAT_REGEXP = Pattern.compile("^[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?$");
    private final static Pattern INTEGER_REGEXP = Pattern.compile("^[-+]?[0-9]+$");
    private final static Pattern FILESIZE_REGEXP = Pattern.compile("^[-+]?[0-9]+\\s*[gGkKmMгГмМкКtTтТ]?[bBбБ]?\\s*$");

    private boolean onlyType(final List<SchemaField> fields, final String type) {
        final Set<String> classNames = new HashSet<String>();
        for (final SchemaField field : fields) {
            classNames.addAll(field.ranges);
        }
        for(String addType: MicrodataUtils.protocolAliases(type)) {
            if (!classNames.remove(addType)){
                return false;
            }
        }
        return classNames.isEmpty();
    }

    private boolean checkClass(final SchemaClass chkCls, final List<String> ranges) {
        for (final String range : ranges) {
            if (range.equals(chkCls.name)) {
                return true;
            }
        }
        if (chkCls != null) {
            for (final SchemaClass parent : chkCls.subClassOf) {
                if (checkClass(parent, ranges)) {
                    return true;
                }
            }
        }
        return false;
    }
}
