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

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import ru.yandex.common.util.Su;
import ru.yandex.common.util.XmlUtils;
import ru.yandex.common.util.collections.Pair;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.data.MFAnyData;
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.*;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.exceptions.*;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.MFProperty;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.MFType;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.Microformat;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.property_types.*;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
 * Created by IntelliJ IDEA.
 * User: rasifiel
 * Date: 7/15/11
 * Time: 2:24 PM
 * To change this template use File | Settings | File Templates.
 */
public class ParseContext implements Context<MicroformatData> {

    final LinkedList<Document> nodes;
    List<MicroformatData> result;
    final DocumentProperties documentProperties;
    final MFExceptions validatorExceptions = new MFExceptions("");

    public ParseContext(final List<Document> nodes, final DocumentProperties documentProperties) {
        this.nodes = new LinkedList<Document>();
        for (final Document node : nodes) {
            this.nodes.add(node);
        }
        this.documentProperties = documentProperties;
    }

    @Override
    public TransformationStep getStep() {
        return TransformationStep.PARSE;
    }

    @Override
    public List<MicroformatData> getInfo() {
        if (result == null) {
            result = new LinkedList<MicroformatData>();
            for (final Document doc : nodes) {
                final Node root = doc.getDocumentElement();
                relResolve(root);
                deepParse(root, result);
            }
            result = Collections.unmodifiableList(result);
            documentProperties.putEx(validatorExceptions);
        }
        return result;
    }

    private void relResolve(final Node root) {
        if (root instanceof Element) {
            final Element er = (Element) root;
            if (!er.getAttribute("rel").isEmpty()) {
                final StringBuilder sb = new StringBuilder(er.getAttribute("class"));
                for (final String rel : Tu.splitBySpaces(er.getAttribute("rel"))) {
                    sb.append(" ").append("rel-").append(rel);
                }
                er.setAttribute("class", sb.toString());
            }
        }
        Node nd = root.getFirstChild();
        while (nd != null) {
            relResolve(nd);
            nd = nd.getNextSibling();
        }
    }

    private void deepParse(final Node root, final List<MicroformatData> result) {
        if (root instanceof Element) {
            while (true) {
                final List<Microformat> formats = Tu.getRootMFsForClasses(((Element) root).getAttribute("class"),
                        documentProperties.getManager());
                if (formats.isEmpty()) {
                    break;
                }
                Microformat format = formats.get(0);
                if (format.isRoot()) {
                    try {
                        final List<Pair<Element, String>> toRemove = new ArrayList<Pair<Element, String>>();
                        final MFExceptions exceptions = new MFExceptions("");
                        final MicroformatData data = parseNodeAsMicroformat(root, format, toRemove, exceptions);
                        final List<MFException> exceptionList = exceptions.getExceptionsByListView();
                        for (final MFException ex : exceptionList) {
                            //ex.setCard(data);
                            validatorExceptions.put(ex);
                        }
                        if (data != null) {
                            if (documentProperties.isExpandTree()) {
                                deepAdd(data, result, true);
                            } else {
                                result.add(data);
                            }
                        }
                        for (final Pair<Element, String> entry : toRemove) {
                            entry.first.setAttribute("class",
                                    Tu.stripClassFromClasses(entry.first.getAttribute("class"), entry.second));
                        }
                    } catch (MaxDepthMFException e) {
                        return;
                    } catch (InvalidActionException e) {
                        //Can't be
                    }
                }
            }
        }
        Node nd = root.getFirstChild();
        while (nd != null) {
            deepParse(nd, result);
            nd = nd.getNextSibling();
        }
    }

    private void deepAdd(final MicroformatData data, final List<MicroformatData> result, boolean root) {
        if (data.isRoot()) {
            result.add(data);
        }
        for (final MFProperty property : data.getProperties()) {
            for (final MFAnyData anyData : data.getDataAsList(property.getName())) {
                if (anyData instanceof MicroformatData) {
                    deepAdd((MicroformatData) anyData, result, false);
                }
            }
        }
    }

    private static final Integer STOP_PROCESS = 1;

    private MicroformatData parseNodeAsMicroformat(final Node root, final Microformat mf, final List<Pair<Element, String>> toRemove, final MFExceptions validatorExceptions) throws InvalidActionException, MaxDepthMFException {
        final MFExceptions exceptions = new MFExceptions(mf.getName());
        if (root instanceof Element) {
            final Element er = (Element) root;
            er.setAttribute("class", Tu.stripClassFromClasses(er.getAttribute("class"), mf.getName()));
            toRemove.add(new Pair<Element, String>(er, mf.getName()));
        }
        final List<MFProperty> properties = mf.getProperties();
        final MicroformatData resultData = mf.createData();
        exceptions.setCard(resultData);
        if (mf.isRoot()) {
            final String textRepr = XmlUtils.unescapeXml(
                    Su.leaveOnlyOneSpaceCharBetweenWords(Tu._stringifyNode(root, false, true)) + " ");
            resultData.addUndefinedSingularTextData("raw", textRepr);
        }
        for (final MFProperty prop : properties) {
            final List<MFType> types = prop.getPossibleTypes();
            final LinkedList<Microformat> canBeMF = new LinkedList<Microformat>();
            for (final MFType type : types) {
                if (type instanceof Microformat) {
                    final Microformat microformatType = (Microformat) type;
                    canBeMF.add(microformatType);
                }
            }
            List<Element> propFields = Tu.findByClass(root, prop.getName(), true);
            if (propFields.isEmpty()) {
                continue;
            }
            try {
                propFields = Tu.filterParent(propFields, root, mf, prop, documentProperties);
            } catch (MaxDepthMFException e) {
                exceptions.put(e);
                validatorExceptions.put(exceptions);
                throw e;
            }
            if (propFields.isEmpty()) {
                continue;
            }

            for (final Element propField : propFields) {
                toRemove.add(new Pair<Element, String>(propField, prop.getName()));
                boolean found = false;
                for (final Microformat curMF : canBeMF) {
                    final MFExceptions propertyExceptions =
                            new MFExceptions(prop.getName().equals(curMF.getName()) ? "" : prop.getName());
                    List<Element> canBeValueNodes = Tu.findByClassNotAnyIncluded(propField, curMF.getName(), true);
                    canBeValueNodes = Tu.filterParent(canBeValueNodes, propField, prop, curMF, documentProperties);
                    for (final Element field : canBeValueNodes) {
                        if (curMF != null && (field != root || curMF != mf)) {
                            final String tmp = field.getAttribute("class");
                            field.setAttribute("class", Tu.stripClassFromClasses(tmp, prop.getName()));
                            final MicroformatData data;
                            try {
                                data = parseNodeAsMicroformat(field, curMF, toRemove, propertyExceptions);
                            } catch (MaxDepthMFException e) {
                                exceptions.put(propertyExceptions);
                                validatorExceptions.put(exceptions);
                                throw e;
                            }
                            if (data != null) {
                                boolean success = false;
                                try {
                                    resultData.addData(prop, data);
                                    success = true;
                                } catch (InvalidDataMFException e) {
                                    if (!e.isCritical()) {
                                        exceptions.put(e);
                                        success = true;
                                    }
                                }
                                if (success) {
                                    found = true;
                                }
                            }
                            field.setAttribute("class", tmp);
                        }
                    }
                    exceptions.put(propertyExceptions);
                    if (found) {
                        break;
                    }
                }

                if (!found) {
                    extractSimpleData(exceptions, resultData, prop, types, propField);
                }
            }
        }

        final String content = Tu.stringifyNode(root, true, true, true);
        try {
            mf.postProcess(resultData, content, root);
        } catch (EmptyMFException e) {
            return null;
        } catch (MFExceptions e) {
            if (resultData.isEmpty() && !mf.isRoot()) {
                return null;
            } else {
                exceptions.concatenate(e);
            }
        }
        for (final MFException ex : exceptions.getExceptionsByListView()) {
            if (ex.getCard() == null) {
                ex.setCard(resultData);
            }
        }
        validatorExceptions.put(exceptions);
        return resultData;
    }

    private void extractSimpleData(final MFExceptions exceptions, final MicroformatData resultData, final MFProperty prop, final List<MFType> types, final Element propField) {
        for (final MFType type : types) {
            if (!(type instanceof Microformat)) {
                if (type instanceof FlagProperty) {
                    try {
                        resultData.addData(prop, new FlagData());
                        break;
                    } catch (InvalidDataMFException e) {
                        if (!e.isCritical()) {
                            exceptions.put(e);
                            break;
                        }
                    }
                } else if (type instanceof URIProperty) {
                    final String url = Tu.getUriValue(propField, false);
                    try {
                        resultData.addData(prop, new URIData(url));
                        break;
                    } catch (InvalidDataMFException e) {
                        if (!e.isCritical()) {
                            exceptions.put(e);
                            break;
                        }
                    }
                } else if (type instanceof TelURIProperty) {
                    final String url = Tu.getUriValue(propField, false);
                    try {
                        resultData.addData(prop, new TelURIData(url));
                        break;
                    } catch (InvalidDataMFException e) {
                        if (!e.isCritical()) {
                            exceptions.put(e);
                            break;
                        }
                    }
                } else if (type instanceof IDProperty) {
                    final String url = Tu.getUriValue(propField, true);
                    try {
                        resultData.addData(prop, new IDData(url));
                        break;
                    } catch (InvalidDataMFException e) {
                        if (!e.isCritical()) {
                            exceptions.put(e);
                            break;
                        }
                    }
                } else if (type instanceof HTMLProperty) {
                    final String html = Tu.extractHtmlContent(propField);
                    try {
                        resultData.addData(prop, new HTMLData(html));
                        propField.setUserData("stop", STOP_PROCESS, null);
                        break;
                    } catch (InvalidDataMFException e) {
                        if (!e.isCritical()) {
                            exceptions.put(e);
                            break;
                        }
                    }
                } else if (type instanceof TextProperty) {
                    final String text = XmlUtils.unescapeXml(Tu.stringifyNode(propField, true, true, false));
                    try {
                        resultData.addData(prop, new TextData(text));
                        propField.setUserData("stop", STOP_PROCESS, null);
                        break;
                    } catch (InvalidDataMFException e) {
                        if (!e.isCritical()) {
                            exceptions.put(e);
                            break;
                        }
                    }
                } else if (type instanceof DatetimeProperty) {
                    final String text = Tu.getDatetimeContent(propField);
                    try {
                        resultData.addData(prop, new DatetimeData(text));
                        break;
                    } catch (InvalidDataMFException e) {
                        if (!e.isCritical()) {
                            exceptions.put(e);
                            break;
                        }
                    }
                }
            }
        }
    }

    @Override
    public Context<?> nextStep() {
        return new FinalContext(getInfo(), this.documentProperties);
    }
}
