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

import org.htmlcleaner.TagNode;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Node;
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.exceptions.*;

import java.util.*;

abstract public class Microformat implements ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.MFType {

    private List<MFProperty> properties;
    /**
     * e.g. vcard
     */
    private final String name;
    private boolean frozen = false;
    private Map<String, MFProperty> mapProperty = new HashMap<String, MFProperty>();
    private final boolean root;

    /**
     * the name should be equal to the header of microformat (e. g. "vcard")
     *
     * @param name
     */
    protected Microformat(@NotNull final String name, final boolean root) {
        this.name = name;
        properties = new ArrayList<MFProperty>();
        this.root = root;
    }

    public List<MFProperty> getProperties() throws InvalidActionException {
        if (!frozen) {
            throw new InvalidActionException("not frozen yet");
        }
        return properties;
    }

    @Nullable
    public MFProperty getProperty(@NotNull final String propertyName) throws InvalidActionException {
        if (!frozen) {
            throw new InvalidActionException("not frozen yet");
        }
        return mapProperty.get(propertyName);
    }

    public String getName() throws InvalidActionException {
        if (!frozen) {
            throw new InvalidActionException("not frozen yet");
        }
        return name;
    }

    public boolean isRoot() {
        return root;
    }

    // === implementations

    /**
     * Prepare object for use
     */
    protected void freeze() {
        frozen = true;
        properties = Collections.unmodifiableList(properties);
        mapProperty = Collections.unmodifiableMap(mapProperty);
    }

    protected Microformat addProperty(@NotNull final MFProperty property) throws InvalidActionException {
        if (frozen) {
            throw new InvalidActionException("already frozen");
        }
        if (mapProperty.get(property.getName()) != null) {
            throw new InvalidActionException(property.getName() + " already exists");
        }
        mapProperty.put(property.getName(), property);
        properties.add(property);
        return this;
    }

    public MicroformatData createData() throws InvalidActionException {
        if (!frozen) {
            throw new InvalidActionException("not frozen yet");
        }
        return new MicroformatData(this);
    }

    /**
     * to short representation in one line without property names
     *
     * @param data
     * @return
     * @throws InvalidActionException
     *
     */
    public String toShortString(@NotNull final MicroformatData data) throws InvalidActionException {
        if (!frozen) {
            throw new InvalidActionException("not frozen yet");
        }
        final StringBuilder sb = new StringBuilder();
        for (final MFProperty property : properties) {
            if (data.isEmpty(property.getName())) {
                continue;
            }
            if (sb.length() > 0) {
                sb.append(" ");
            }
            sb.append(property.toShortString(data.get(property.getName())));
        }
        return sb.toString();
    }

    public String toDebugString(@NotNull final MicroformatData data, @NotNull final String indent) throws InvalidActionException {
        if (!frozen) {
            throw new InvalidActionException("not frozen yet");
        }
        final StringBuilder sb = new StringBuilder();
        sb.append("{\n");
        final String newIndent = indent + "  ";
        for (final MFProperty property : properties) {
            final String fname = property.getName();
            if (data.isEmpty(fname)) {
                continue;
            }
            sb.append(newIndent).append(fname).append(" = ");
            sb.append(property.toDebugString(data.get(fname), newIndent));
            sb.append("\n");
        }
        sb.append(indent).append("}");
        return sb.toString();
    }

    public String toShortString(@NotNull final MFAnyData data) throws InvalidActionException {
        return toShortString((MicroformatData) data);
    }

    public String toDebugString(@NotNull final MFAnyData data, @NotNull final String indent) throws InvalidActionException {
        return toDebugString((MicroformatData) data, indent);
    }

    /**
     * this method is called after parsing of particular microformat is ended
     * the default method checks if the data is not empty and calls postProcess methods for each property
     *
     * @param data
     * @param content
     * @param node
     * @return true if the content was used
     * @throws MFException
     */
    public boolean postProcess(@NotNull final MicroformatData data, @NotNull final String content, final Node node) throws InvalidActionException, EmptyMFException, MFExceptions {
        if (!frozen) {
            throw new InvalidActionException("not frozen yet");
        }
        boolean res = false;
        final MFExceptions exs = new MFExceptions(getName());
        for (final MFProperty property : properties) {
            try {
                res |= postProcess(data, property, content, node);
            } catch (MFException ex) {
                exs.put(ex);
            }
        }
        if (!exs.isEmpty()) {
            throw exs;
        }
        if (data.isEmpty()) {
            throw new EmptyMFException(getName());
        }
        return res;
    }

    public boolean postProcess(@NotNull final MicroformatData data, @NotNull final String content, final TagNode node) throws InvalidActionException, EmptyMFException, MFExceptions {
        if (!frozen) {
            throw new InvalidActionException("not frozen yet");
        }
        boolean res = false;
        final MFExceptions exs = new MFExceptions(getName());
        for (final MFProperty property : properties) {
            try {
                res |= postProcess(data, property, content, node);
            } catch (MFException ex) {
                exs.put(ex);
            }
        }
        if (!exs.isEmpty()) {
            throw exs;
        }
        if (data.isEmpty()) {
            throw new EmptyMFException(getName());
        }
        return res;
    }

    public synchronized boolean postProcess(@NotNull final MicroformatData data, @NotNull final MFProperty property, @NotNull final String content, final Node node) throws MFException, InvalidActionException {
        if (!frozen) {
            throw new InvalidActionException("not frozen yet");
        }
//        if (data.get(property.getName()) == null) {
//            //data.put(property.getName(), property.createData());
//            data.giveMethodsTo(this);
//            methods.put(property.getName(), property.createData());
//            methods = null;
//        }
        return property.postProcess(data.get(property.getName()), content, node, data);
    }

    public synchronized boolean postProcess(@NotNull final MicroformatData data, @NotNull final MFProperty property, @NotNull final String content, final TagNode node) throws MFException, InvalidActionException {
        if (!frozen) {
            throw new InvalidActionException("not frozen yet");
        }
//        if (data.get(property.getName()) == null) {
//            //data.put(property.getName(), property.createData());
//            data.giveMethodsTo(this);
//            methods.put(property.getName(), property.createData());
//            methods = null;
//        }
        return property.postProcess(data.get(property.getName()), content, node, data);
    }

    public boolean postProcess(@NotNull final MicroformatData data, @NotNull final String propertyName, @NotNull final String content, final Node node) throws MFException, InvalidActionException {
        if (mapProperty.get(propertyName) == null) {
            throw new InvalidActionException(propertyName + " does not exist");
        }
        return postProcess(data, mapProperty.get(propertyName), content, node);
    }

    synchronized public void addData(@NotNull final MicroformatData data, @NotNull final MFProperty property, @NotNull final MFAnyData newData) throws InvalidDataMFException, InvalidActionException {
        if (!frozen) {
            throw new InvalidActionException("not frozen yet");
        }
        if (newData.isEmpty()) {
            throw new EmptyMFException(property.getName(), property.getLocation());
        }
        if (data.get(property.getName()) == null) {
            //data.put(property.getName(), property.createData());
            data.giveMethodsTo(this);
            methods.put(property.getName(), property.createData());
            methods = null;
        }
        property.addData(data.get(property.getName()), newData);
    }

    public void addData(@NotNull final MicroformatData data, @NotNull final String propertyName, @NotNull final MFAnyData newData) throws InvalidDataMFException, InvalidActionException {
        if (mapProperty.get(propertyName) == null) {
            throw new InvalidActionException(propertyName + " does not exist");
        }
        addData(data, mapProperty.get(propertyName), newData);
    }

    private MicroformatData.Methods methods; //temporal data

    synchronized public void receiveMethods(final MicroformatData.Methods methods) {
        this.methods = methods;
    }
}
