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

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.json.*;
import ru.yandex.common.util.Su;
import ru.yandex.common.util.XmlUtils;
import ru.yandex.common.util.collections.*;
import ru.yandex.common.util.functional.Function;
import ru.yandex.common.util.functional.PartialFunction;
import ru.yandex.common.util.xml.Xmler;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.data.MFAnyData;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.data.MFPropertyData;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.data.MicroformatData;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.data.property_types.HTMLData;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.exceptions.InvalidActionException;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.exceptions.MFException;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.MFProperty;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.MicroformatsManager;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.transformer.*;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.serialize.util.APIVersion;

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

import static ru.yandex.common.util.xml.Xmler.tag;
import static ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.FrontEnd.cropString;
import static ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.FrontEnd.verifierNode;

/**
 * Created by IntelliJ IDEA.
 * User: rasifiel
 * Date: 8/2/11
 * Time: 6:55 PM
 * To change this template use File | Settings | File Templates.
 */
public class MicroformatsUtils {

    private static Pattern STRIP_TAGS = Pattern.compile("\\<.*?>");

    public static Xmler.Tag toXmlTag(final MicroformatData data, final String rootName, final boolean firstLevel) {
        final List<Xmler.Tag> tags = new LinkedList<Xmler.Tag>();
        final List<MFAnyData> vals = data.getDataAsList("value");
        if (!vals.isEmpty()) {
            for (final MFAnyData v : vals) {
                final String valueString = cropString(XmlUtils.unescapeXml(v.toString()).trim());
                if (!valueString.isEmpty()) {
                    tags.add(tag("v", valueString));
                }
            }
        }
        for (final MFProperty prop : data.getProperties()) {
            if (!"raw".equals(prop.getName()) && !"value".equals(prop.getName())) {
                final List<MFAnyData> datas = data.getDataAsList(prop.getName());
                for (final MFAnyData element : datas) {
                    if (element instanceof MicroformatData) {
                        if (((MicroformatData) element).isRoot() &&
                                !prop.getName().equals(((MicroformatData) element).getName())) {
                            tags.add(verifierNode(prop.getName(), null, false,
                                    toXmlTag((MicroformatData) element, null, false)));
                        } else {
                            tags.add(toXmlTag((MicroformatData) element, prop.getName(), false));
                        }
                    } else if (element instanceof HTMLData) {
                        tags.add(verifierNode(prop.getName(), null, false, tag("v", cropString(Su.normalizeWhitespace(
                                XmlUtils.unescapeXml(STRIP_TAGS.matcher(element.toString()).replaceAll("")).replaceAll(
                                        "&#\\d+;", " "))))));
                    } else {
                        tags.add(verifierNode(prop.getName(), null, false,
                                tag("v", cropString(XmlUtils.unescapeXml(element.toString())))));
                    }
                }
            }
        }
        if (rootName == null || rootName.equals(data.getName())) {
            return verifierNode(data.getName(), data.number, firstLevel, tags);
        } else {
            return verifierNode(rootName, null, firstLevel, tags);
        }
    }

    public static void toXml(final MicroformatData data, final StringBuilder sb) {
        toXmlTag(data, null, false).toXml(sb);
        sb.append("\n");
    }

    public static Pair<List<MicroformatData>, List<MFException>> extractMF(final String content, final String url, final MicroformatsManager manager, final boolean extractDeepCards) {
        final TransformStrategy strategy = new TransformStrategy();
        final DocumentProperties properties = new DocumentProperties(url, manager, "UTF-8", extractDeepCards);
        final DocumentContext startContext = new DocumentContext(content, properties);
        final Context result = strategy.processTillDone(startContext);
        if (result instanceof FinalContext) {
            final FinalContext resultAsFinalContext = (FinalContext) result;
            return new Pair<List<MicroformatData>, List<MFException>>(resultAsFinalContext.getInfo(),
                    resultAsFinalContext.getExs());
        } else {
            return new Pair<List<MicroformatData>, List<MFException>>(Collections.<MicroformatData>emptyList(),
                    Collections.<MFException>emptyList());
        }
    }

    public static Xmler.Tag toExportXml(@NotNull final MicroformatData card, @Nullable final String rootName) {
        final List<Xmler.Tag> tags = new LinkedList<Xmler.Tag>();
        for (final MFProperty property : card.getProperties()) {
            final List<MFAnyData> data = card.getDataAsList(property.getName());
            for (final MFAnyData element : data) {
                if (element instanceof MicroformatData) {
                    tags.add(toExportXml((MicroformatData) element, property.getName()));
                } else {
                    tags.add(tag(property.getName(), element.toString()));
                }
            }
        }
        if (rootName == null) {
            try {
                return tag(card.getSpec().getName(), tags);
            } catch (InvalidActionException e) {
                //ignore
                return null;
            }
        } else {
            return tag(rootName, tags);
        }
    }

    @Nullable
    public static Either<Xmler.Tag, String> toExport(@Nullable final MFAnyData data, @Nullable final String rootName) {
        if (data == null) {
            return null;
        }
        if (data instanceof MicroformatData) {
            return Either.left(toExportXml((MicroformatData) data, rootName));
        } else {
            return Either.right(data.toShortString());
        }
    }

    @Nullable
    public static String toExportString(@Nullable final MFAnyData data) {
        if (data == null) {
            return null;
        }
        return data.toShortString();
    }

    public static List<String> extractPropertyToListString(@NotNull final MicroformatData data, @NotNull final String propertyName) {
        final List<MFAnyData> property = data.getDataAsList(propertyName);
        final Function<MFAnyData, String> func = new Function<MFAnyData, String>() {
            @Override
            public String apply(final MFAnyData arg) {
                return toExportString(arg);
            }
        };
        return func.map(property);
    }

    public static List<Xmler.Tag> extractPropertyToListXML(@NotNull final MicroformatData data, @NotNull final String propertyName) {
        final List<MFAnyData> property = data.getDataAsList(propertyName);
        final PartialFunction<MFAnyData, Xmler.Tag> func = new PartialFunction<MFAnyData, Xmler.Tag>() {
            @Override
            public Xmler.Tag apply(final MFAnyData arg) {
                if (!(arg instanceof MicroformatData)) {
                    throw new IllegalArgumentException();
                }
                return toExportXml((MicroformatData) arg, propertyName);
            }
        };
        return Cu.mapWhereDefined(func, property);
    }

    public static List<Xmler.Tag> extractPropertyToListXML(@NotNull final MicroformatData data, @NotNull final String propertyName, @NotNull final Function<String, Xmler.Tag> fromString) {
        final List<MFAnyData> property = data.getDataAsList(propertyName);
        final Function<MFAnyData, Xmler.Tag> func = new Function<MFAnyData, Xmler.Tag>() {
            @Override
            public Xmler.Tag apply(final MFAnyData arg) {
                if (!(arg instanceof MicroformatData)) {
                    return fromString.apply(toExportString(arg));
                }
                return toExportXml((MicroformatData) arg, propertyName);
            }
        };
        return Cu.map(func, property);
    }

    public static List<Xmler.Tag> extractPropertyAndTextToListXML(@NotNull final MicroformatData data, @NotNull final String propertyName) {
        return extractPropertyToListXML(data, propertyName, new Function<String, Xmler.Tag>() {
            @Override
            public Xmler.Tag apply(final String arg) {
                return tag(propertyName, arg);
            }
        });
    }

    public static List<Either<Xmler.Tag, String>> extractPropertyToList(@NotNull final MicroformatData data, @NotNull final String propertyName) {
        final List<MFAnyData> property = data.getDataAsList(propertyName);
        final Function<MFAnyData, Either<Xmler.Tag, String>> func =
                new Function<MFAnyData, Either<Xmler.Tag, String>>() {
                    @Override
                    public Either<Xmler.Tag, String> apply(final MFAnyData arg) {
                        return toExport(arg, propertyName);
                    }
                };
        return func.map(property);
    }

    public static <X> List<X> extractPropertyToList(@NotNull final MicroformatData data, @NotNull final String propertyName, @NotNull final PartialFunction<MFAnyData, X> func) {
        final List<MFAnyData> property = data.getDataAsList(propertyName);
        return Cu.mapWhereDefined(func, property);
    }

    @Nullable
    public static String extractPropertyToSingleString(@NotNull final MicroformatData data, @NotNull final String propertyName) {
        final MFAnyData property = data.getFirstOrNull(propertyName);
        return toExportString(property);
    }

    @Nullable
    public static Xmler.Tag extractPropertyToSingleXml(@NotNull final MicroformatData data, @NotNull final String propertyName) {
        final MFAnyData property = data.getFirstOrNull(propertyName);
        if (property == null || !(property instanceof MicroformatData)) {
            return null;
        }
        return toExportXml((MicroformatData) property, propertyName);
    }

    @Nullable
    public static Xmler.Tag extractPropertyToSingleXml(@NotNull final MicroformatData data, @NotNull final String propertyName, @NotNull final Function<String, Xmler.Tag> fromString) {
        final MFAnyData property = data.getFirstOrNull(propertyName);
        if (property == null) {
            return null;
        }
        if (property instanceof MicroformatData) {
            return toExportXml((MicroformatData) property, propertyName);
        }
        return fromString.apply(toExportString(property));
    }

    @Nullable
    public static Either<Xmler.Tag, String> extractPropertyToSingle(@NotNull final MicroformatData data, @NotNull final String propertyName) {
        final MFAnyData property = data.getFirstOrNull(propertyName);
        return toExport(property, propertyName);
    }

    @Nullable
    public static <X> X extractPropertyToSingle(@NotNull final MicroformatData data, @NotNull final String propertyName, @NotNull final Function<MFAnyData, X> func) {
        final MFAnyData property = data.getFirstOrNull(propertyName);
        return func.apply(property);
    }

    public static JSONObject toJson(final MicroformatData data, final MultiMap<Integer, JSONObject> exceptions, final boolean only_errors, APIVersion version) {
        final Map<String, Object> result;
        try {
            result = Cu.mapFromIterable(new Function<Map.Entry<String, MFPropertyData>, String>() {
                                            @Override
                                            public String apply(final Map.Entry<String, MFPropertyData> arg) {
                                                return arg.getKey();
                                            }
                                        }, new Function<Map.Entry<String, MFPropertyData>, Object>() {
                                            @Override
                                            public Object apply(final Map.Entry<String, MFPropertyData> arg) {
                                                final JSONArray result = new JSONArray();
                                                for (final MFAnyData data : arg.getValue().getDataAsList()) {
                                                    if (data != null) {
                                                        if (data instanceof MicroformatData) {
                                                            result.put(toJson((MicroformatData) data, exceptions,only_errors, version));
                                                        } else if(!only_errors){
                                                            switch (version){
                                                                case VERSION_1_1:
                                                                    try {
                                                                        result.put(new JSONObject().put("@value", data.toShortString()).put("#location", arg.getValue().getLocation()));
                                                                    } catch (JSONException e) {
                                                                        throw new RuntimeException(e);
                                                                    }
                                                                    break;
                                                                default:
                                                                    result.put(data.toShortString());
                                                            }
                                                        }
                                                    }
                                                }
                                                return result;
                                            }
                                        }, data.getData().entrySet()
                                       );
            final LinkedList<String> toRemove = new LinkedList<String>();
            for (final String x : result.keySet()) {
                if (((JSONArray) result.get(x)).length() == 0) {
                    toRemove.add(x);
                }
            }
            for (final String x : toRemove) {
                result.remove(x);
            }
            result.remove("raw");
            if (exceptions != null) {
                List errors = new ArrayList<>();
                if(exceptions.containsKey(data.number)) {
                    for (JSONObject e : exceptions.get(data.number)) {
                        errors.add(APIVersion.serializeToArrayForOldVersion(version,e));
                    }
                    result.put(APIVersion.getErrorKey(version), errors);
                }

            }
            if(!only_errors) {
                result.put("@type", Cf.list(data.getSpec().getName()));
            }
            return new JSONObject(result);
        } catch (InvalidActionException e) {
            throw new RuntimeException(e);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }
}
