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

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Node;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.location.EntityLocation;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.data.property_types.TextData;
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.MFPropertySingular;
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.TextProperty;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

public class MicroformatData extends MFAnyData {

    private static final int START_COUNT = Integer.MIN_VALUE;

    private static final AtomicInteger counter = new AtomicInteger(START_COUNT);

    public final int number = counter.incrementAndGet();


    private EntityLocation location;

    public EntityLocation getLocation() {
        return location;
    }

    public void setLocation(EntityLocation location) {
        this.location = location;
    }

    private final Map<String, MFPropertyData> data = new HashMap<String, MFPropertyData>();
    private final Microformat spec;

    public MicroformatData(@NotNull final Microformat spec) {
        this.spec = spec;
    }

    /**
     * Deprecated. Use addData instead
     *
     * @param propertyName
     * @param mfPropertyData
     * @return
     */
    @Deprecated
    public MFPropertyData put(@NotNull final String propertyName, @NotNull final MFPropertyData mfPropertyData) {
        return data.put(propertyName, mfPropertyData);
    }

    public void addUndefinedSingularTextData(@NotNull final String propertyName, @NotNull final String text) throws InvalidActionException {
        if (data.get(propertyName) != null || getProperty(propertyName) != null) {
            throw new InvalidActionException(propertyName + " already exists");
        }
        final MFProperty prop = new MFPropertySingular(propertyName, TextProperty.getInstance());
        final TextData newData = new TextData(text);
        put(propertyName, prop.createData());
        try {
            final MFPropertyData data = get(prop.getName());
            if (data != null) {
                prop.addData(data, newData);
            }
        } catch (InvalidDataMFException e) {
            //cannot be
        }
    }

    @Nullable
    public MFPropertyData get(@NotNull final String propertyName) {
        return data.get(propertyName);
    }

    public List<MFAnyData> getDataAsList(@NotNull final String propertyName) {
        if (isEmpty(propertyName)) {
            return Collections.emptyList();
        }
        return get(propertyName).getDataAsList();
    }

    public MFAnyData getFirstOrNull(@NotNull final String propertyName) {
        if (isEmpty(propertyName)) {
            return null;
        }
        return get(propertyName).getDataAsList().get(0);
    }

    public Map<String, MFPropertyData> getData() {
        return Collections.unmodifiableMap(data);
    }

    @Override
    public Microformat getSpec() {
        return spec;
    }

    @Override
    public boolean isEmpty() {
        for (final MFPropertyData propertyData : data.values()) {
            if (!propertyData.isEmpty()) {
                return false;
            }
        }
        return true;
    }

    @Override
    public MicroformatData clone() {
        final MicroformatData res = new MicroformatData(getSpec());
        for (final Map.Entry<String, MFPropertyData> entry : data.entrySet()) {
            res.put(entry.getKey(), entry.getValue().clone());
        }
        return res;
    }

    public boolean isEmpty(@NotNull final String propertyName) {
        return get(propertyName) == null || get(propertyName).isEmpty();
    }

    public boolean containsOnly(@NotNull final String propertyName) {
        return data.size() == 1 && !isEmpty(propertyName);
    }

    public MFPropertyData remove(final String propertyName) {
        return data.remove(propertyName);
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof MicroformatData)) {
            return false;
        }

        final MicroformatData that = (MicroformatData) o;

        try {
            for (final MFProperty property : getSpec().getProperties()) {
                final String name = property.getName();
                final boolean empty = isEmpty(name);
                if (empty != that.isEmpty(name) || (!empty && !get(name).equals(that.get(name)))) {
                    return false;
                }
            }
        } catch (InvalidActionException e) {
            e.printStackTrace();
        }

        return true;
    }

    @Override
    public int hashCode() {
        if (isEmpty()) {
            return 0;
        }
        final int prime = 113;
        int res = 1;
        try {
            for (final MFProperty property : getSpec().getProperties()) {
                final String name = property.getName();
                final boolean empty = isEmpty(name);
                res *= prime;
                if (!empty) {
                    res += get(name).hashCode();
                }
            }
        } catch (InvalidActionException e) {
            e.printStackTrace();
        }
        return res;
    }

    @Override
    public String toString() {
        try {
            return getSpec().toDebugString(this, "");
        } catch (InvalidActionException e) {
            e.printStackTrace();
        }
        return "";
    }

    public List<MFProperty> getProperties() {
        try {
            return getSpec().getProperties();
        } catch (InvalidActionException e) {
            e.printStackTrace();  //cannot be
            return null;
        }
    }

    @Nullable
    public MFProperty getProperty(@NotNull final String propertyName) {
        try {
            return getSpec().getProperty(propertyName);
        } catch (InvalidActionException e) {
            e.printStackTrace();  //cannot be
            return null;
        }
    }

    public String getName() {
        try {
            return getSpec().getName();
        } catch (InvalidActionException e) {
            e.printStackTrace();  //cannot be
            return null;
        }
    }

    public boolean isRoot() {
        return getSpec().isRoot();
    }

    public String toShortString() {
        try {
            return getSpec().toShortString(this);
        } catch (InvalidActionException e) {
            e.printStackTrace();  //cannot be
            return null;
        }
    }

    public boolean postProcess(@NotNull final String content, final Node node) throws InvalidActionException, EmptyMFException, MFExceptions {
        return spec.postProcess(this, content, node);
    }

    public boolean postProcess(@NotNull final MFProperty property, @NotNull final String content, final Node node) throws MFException, InvalidActionException {
        return spec.postProcess(this, property, content, node);
    }

    public boolean postProcess(@NotNull final String propertyName, @NotNull final String content, final Node node) throws MFException, InvalidActionException {
        return spec.postProcess(this, propertyName, content, node);
    }

    public void addData(@NotNull final MFProperty property, @NotNull final MFAnyData newData) throws InvalidDataMFException {
        try {
            getSpec().addData(this, property, newData);
        } catch (InvalidActionException e) {
            e.printStackTrace();  //cannot be
        }
    }

    public void addData(@NotNull final String propertyName, @NotNull final MFAnyData newData) throws InvalidDataMFException, InvalidActionException {
        getSpec().addData(this, propertyName, newData);
    }

    public void giveMethodsTo(final Microformat microformat) {
        microformat.receiveMethods(new Methods());
    }

    public class Methods {
        private Methods() {
        }

        public MFPropertyData put(@NotNull final String propertyName, @NotNull final MFPropertyData mfPropertyData) {
            return MicroformatData.this.put(propertyName, mfPropertyData);
        }
    }
}
