package ru.yandex.autodoc.common.doc.view.handlers;

import ru.yandex.autodoc.common.doc.types.AnyObjectModel;
import ru.yandex.autodoc.common.doc.types.CollectionType;
import ru.yandex.autodoc.common.doc.types.EnumType;
import ru.yandex.autodoc.common.doc.types.FieldDescription;
import ru.yandex.autodoc.common.doc.types.MapType;
import ru.yandex.autodoc.common.doc.types.ObjectModel;
import ru.yandex.autodoc.common.doc.types.PolyObjectModel;
import ru.yandex.autodoc.common.doc.types.ValueType;
import ru.yandex.autodoc.common.doc.view.Markup;
import ru.yandex.autodoc.common.doc.view.format.XmlValue;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * @author avhaliullin
 */
public abstract class AbstractXmlFormatResolver implements ObjectFormatResolver {
    public Markup resolve(AnyObjectModel model) {
        return new Markup.XmlObject(
                resolveObject(
                        new Markup.Text(model.getName()),
                        model
                )
        );
    }

    protected XmlValue.XmlTag resolveObject(Markup tagName, AnyObjectModel aObj) {
        if (aObj instanceof ObjectModel) {
            ObjectModel obj = (ObjectModel) aObj;
            return new XmlValue.MultiLineTag(
                    tagName,
                    resolveFields(obj.getFields()),
                    Markup.textOrEmpty(obj.getDescription())
            );
        } else if (aObj instanceof PolyObjectModel) {
            PolyObjectModel poly = (PolyObjectModel) aObj;
            return new XmlValue.PolyObjectTag(
                    tagName,
                    poly.getDiscriminator(),
                    poly.getCases().stream().map(
                            cse ->
                                    new XmlValue.PolyCase(
                                            cse.getCaseName(),
                                            resolveFields(cse.getModel().getFields()),
                                            Markup.textOrEmpty(cse.getModel().getDescription())
                                    )
                    ).collect(Collectors.toList()),
                    Markup.EMPTY
            );
        } else {
            throw new RuntimeException("Unknown object type " + aObj.getClass());
        }
    }

    protected List<XmlValue> resolveValue(Markup tagName, Markup itemName, ValueType tpe) {
        if (tpe instanceof AnyObjectModel) {
            return Collections.singletonList(resolveObject(tagName, (AnyObjectModel) tpe));
        } else if (tpe instanceof CollectionType) {
            return resolveCollection(tagName, itemName, (CollectionType) tpe);
        } else if (tpe instanceof MapType) {
            MapType map = (MapType) tpe;
            List<XmlValue> items = new ArrayList<>();
            items.addAll(resolveValue(
                    resolveTagName(map.getKeyType()),
                    resolveTagName(map.getKeyType()),
                    map.getValueType()
            ));
            items.add(XmlValue.CONTINUATION);
            return Collections.singletonList(
                    new XmlValue.MultiLineTag(
                            tagName,
                            items,
                            Markup.EMPTY
                    )
            );
        } else {
            return resolvePrimitive(tagName, tpe, tpe);
        }
    }

    protected abstract List<XmlValue> resolveCollection(Markup tagName, Markup itemName, CollectionType tpe);

    protected List<XmlValue> resolvePrimitive(Markup tagName, ValueType baseType, ValueType tpe) {
        Markup description = tpe == baseType
                ? Markup.EMPTY
                : new Markup.Text(tpe.getName());
        if (baseType instanceof EnumType) {
            EnumType enm = (EnumType) baseType;
            return Collections.singletonList(
                    new XmlValue.InlineTag(tagName, TypeMarkupUtil.enumMarkup(enm, true), description, "enum")
            );
        } else if (baseType == ValueType.NUMERIC) {
            return Collections.singletonList(
                    new XmlValue.InlineTag(
                            tagName,
                            new Markup.Text(findExample(tpe).orElse("1")),
                            description,
                            baseType.getName()
                    )
            );
        } else if (baseType == ValueType.STRING) {
            return Collections.singletonList(
                    new XmlValue.InlineTag(
                            tagName,
                            new Markup.Text(findExample(tpe).orElse("some string")),
                            description,
                            baseType.getName()
                    )
            );
        } else if (baseType == ValueType.BOOLEAN) {
            return Collections.singletonList(
                    new XmlValue.InlineTag(
                            tagName,
                            new Markup.Text(findExample(tpe).orElse("false")),
                            description,
                            baseType.getName()
                    )
            );
        } else if (baseType.getBaseType() != null) {
            return resolvePrimitive(tagName, baseType.getBaseType(), tpe);
        } else {
            return Collections.singletonList(
                    new XmlValue.InlineTag(
                            tagName,
                            new Markup.Text(findExample(tpe).orElse("value")),
                            description,
                            baseType.getName()
                    )
            );
        }
    }

    private Optional<String> findExample(ValueType vt) {
        if (vt instanceof ValueType.Primitive) {
            return Optional.of(((ValueType.Primitive) vt).getExample());
        } else if (vt.getBaseType() != null) {
            return findExample(vt.getBaseType());
        } else {
            return Optional.empty();
        }
    }

    private Markup resolveTagName(ValueType vt) {
        if (vt instanceof EnumType) {
            return TypeMarkupUtil.enumMarkup((EnumType) vt, true);
        } else {
            return new Markup.Text(findExample(vt).orElse("some string"));
        }
    }

    private List<XmlValue> resolveFields(List<FieldDescription> fields) {
        return fields.stream().flatMap(f -> resolveField(f).stream()).collect(Collectors.toList());
    }

    protected List<XmlValue> resolveField(FieldDescription f) {
        List<XmlValue> result = new ArrayList<>();
        if (f.getDescription() != null) {
            result.add(new XmlValue.Comment(new Markup.Text(f.getDescription())));
        }
        result.addAll(resolveValue(new Markup.Text(f.getName()), new Markup.Text(f.getItemName()), f.getType()));
        return result;
    }

}
