package org.obolibrary.oboformat.model;

import static org.semanticweb.owlapi.util.OWLAPIPreconditions.verifyNotNull;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.Nullable;

import org.obolibrary.obo2owl.OboInOwlCardinalityTools;
import org.obolibrary.oboformat.parser.OBOFormatConstants.OboFormatTag;

/**
 * The Class Frame.
 */
public class Frame {

    /**
     * The clauses.
     */
    protected Collection<Clause> clauses = new ArrayList<>();
    /**
     * The id.
     */
    @Nullable
    protected String id;
    /**
     * The type.
     */
    @Nullable
    protected FrameType type;

    /**
     * Instantiates a new frame.
     */
    public Frame() {
        this(null);
    }

    /**
     * Instantiates a new frame.
     *
     * @param type the type
     */
    public Frame(@Nullable FrameType type) {
        this.type = type;
    }

    /**
     * freezing a frame signals that a frame has become quiescent, and that data structures can be
     * adjusted to increase performance or reduce memory consumption. If a frozen frame is
     * subsequently modified it will be thawed as necessary.
     */
    public void freeze() {
        if (clauses.isEmpty()) {
            clauses = Collections.emptyList();
            return;
        }
        for (Clause clause : clauses) {
            clause.freeze();
        }
        if (clauses.size() == 1) {
            clauses = Collections.singletonList(clauses.iterator().next());
            return;
        }
        if (clauses instanceof ArrayList<?>) {
            ArrayList<?> arrayList = (ArrayList<?>) clauses;
            arrayList.trimToSize();
        }
    }

    /**
     * @return the type
     */
    @Nullable
    public FrameType getType() {
        return type;
    }

    /**
     * @param type the new type
     */
    public void setType(FrameType type) {
        this.type = type;
    }

    /**
     * @return the id
     */
    @Nullable
    public String getId() {
        return id;
    }

    /**
     * @param id the new id
     */
    public void setId(String id) {
        this.id = id;
    }

    /**
     * @return the clauses
     */
    public Collection<Clause> getClauses() {
        return clauses;
    }

    /**
     * @param clauses the new clauses
     */
    public void setClauses(Collection<Clause> clauses) {
        this.clauses = clauses;
    }

    /**
     * @param tag the tag
     * @return the clauses for tag
     */
    public List<Clause> getClauses(@Nullable String tag) {
        List<Clause> cls = new ArrayList<>();
        if (tag == null) {
            return cls;
        }
        for (Clause cl : clauses) {
            if (tag.equals(cl.getTag())) {
                cls.add(cl);
            }
        }
        return cls;
    }

    /**
     * @param tag the tag
     * @return the clauses for tag
     */
    public List<Clause> getClauses(OboFormatTag tag) {
        return getClauses(tag.getTag());
    }

    /**
     * @param tag the tag
     * @return null if no value set, otherwise first value
     */
    @Nullable
    public Clause getClause(@Nullable String tag) {
        if (tag == null) {
            return null;
        }
        for (Clause cl : clauses) {
            if (tag.equals(cl.getTag())) {
                return cl;
            }
            // TODO - throw exception if more than one clause of this type?
        }
        return null;
    }

    /**
     * @param tag the tag
     * @return the clause for tag
     */
    @Nullable
    public Clause getClause(OboFormatTag tag) {
        return getClause(tag.getTag());
    }

    /**
     * @param cl the clause
     */
    public void addClause(Clause cl) {
        if (!(clauses instanceof ArrayList)) {
            Collection<Clause> tmp = new ArrayList<>(clauses.size() + 1);
            tmp.addAll(clauses);
            clauses = tmp;
        }
        clauses.add(cl);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("Frame(");
        sb.append(id);
        sb.append(' ');
        clauses.forEach(cl -> sb.append(cl).append(' '));
        sb.append(')');
        return sb.toString();
    }

    /**
     * @param tag the tag
     * @return the tag value for tag
     */
    @Nullable
    public Object getTagValue(String tag) {
        Clause clause = getClause(tag);
        if (clause == null) {
            return null;
        }
        return clause.getValue();
    }

    /**
     * @param tag the tag
     * @return the tag value for tag
     */
    @Nullable
    public Object getTagValue(OboFormatTag tag) {
        return getTagValue(tag.getTag());
    }

    /**
     * @param <T> the generic type
     * @param tag the tag
     * @param cls the class
     * @return the tag value for tag and class
     */
    @Nullable
    public <T> T getTagValue(String tag, Class<T> cls) {
        Clause clause = getClause(tag);
        if (clause == null) {
            return null;
        }
        Object value = clause.getValue();
        if (value.getClass().isAssignableFrom(cls)) {
            return cls.cast(value);
        }
        return null;
    }

    /**
     * @param <T> the generic type
     * @param tag the tag
     * @param cls the class
     * @return the tag value for tag and class
     */
    @Nullable
    public <T> T getTagValue(OboFormatTag tag, Class<T> cls) {
        return getTagValue(tag.getTag(), cls);
    }

    /**
     * @param tag the tag
     * @return the tag values for tag
     */
    public Collection<Object> getTagValues(OboFormatTag tag) {
        return getTagValues(tag.getTag());
    }

    /**
     * @param tag the tag
     * @return the tag values for tag
     */
    public Collection<Object> getTagValues(String tag) {
        Collection<Object> vals = new ArrayList<>();
        getClauses(tag).forEach(v -> vals.add(v.getValue()));
        return vals;
    }

    /**
     * @param <T> the generic type
     * @param tag the tag
     * @param cls the class
     * @return the tag values for tag and class
     */
    public <T> Collection<T> getTagValues(OboFormatTag tag, Class<T> cls) {
        return getTagValues(tag.getTag(), cls);
    }

    /**
     * @param <T> the generic type
     * @param tag the tag
     * @param cls the class
     * @return the tag values for tag and class
     */
    public <T> Collection<T> getTagValues(String tag, Class<T> cls) {
        Collection<T> vals = new ArrayList<>();
        getClauses(tag).forEach(c -> vals.add(c.getValue(cls)));
        return vals;
    }

    /**
     * @param tag the tag
     * @return the tag xref list for tag
     */
    public Collection<Xref> getTagXrefs(String tag) {
        Collection<Xref> xrefs = new ArrayList<>();
        Clause clause = getClause(tag);
        if (clause != null) {
            for (Object ob : clause.getValues()) {
                if (ob instanceof Xref) {
                    xrefs.add((Xref) ob);
                }
            }
        }
        return xrefs;
    }

    /**
     * @return the tags
     */
    public Set<String> getTags() {
        Set<String> tags = new HashSet<>();
        getClauses().forEach(cl -> tags.add(cl.getTag()));
        return tags;
    }

    private boolean sameID(Frame f) {
        if (id == null) {
            return f.getId() == null;
        }
        return verifyNotNull(id).equals(f.getId());
    }

    private boolean sameType(Frame f) {
        if (type == null) {
            return f.getType() == null;
        }
        return verifyNotNull(type).equals(f.getType());
    }

    /**
     * @param extFrame the external frame
     * @throws FrameMergeException the frame merge exception
     */
    public void merge(Frame extFrame) throws FrameMergeException {
        if (this == extFrame) {
            return;
        }
        if (!sameID(extFrame)) {
            throw new FrameMergeException("ids do not match");
        }
        if (!sameType(extFrame)) {
            throw new FrameMergeException("frame types do not match");
        }
        extFrame.getClauses().forEach(this::addClause);
        // note we do not perform a document structure check at this point
    }

    /**
     * Check this frame for violations, i.e. cardinality constraint violations.
     *
     * @throws FrameStructureException the frame structure exception
     * @see OboInOwlCardinalityTools for equivalent checks in OWL
     */
    public void check() {
        if (FrameType.HEADER.equals(type)) {
            checkMaxOneCardinality(OboFormatTag.TAG_ONTOLOGY, OboFormatTag.TAG_FORMAT_VERSION,
                OboFormatTag.TAG_DATE, OboFormatTag.TAG_DEFAULT_NAMESPACE,
                OboFormatTag.TAG_SAVED_BY, OboFormatTag.TAG_AUTO_GENERATED_BY);
        }
        if (FrameType.TYPEDEF.equals(type)) {
            checkMaxOneCardinality(OboFormatTag.TAG_DOMAIN, OboFormatTag.TAG_RANGE,
                OboFormatTag.TAG_IS_METADATA_TAG, OboFormatTag.TAG_IS_CLASS_LEVEL_TAG);
        }
        if (!FrameType.HEADER.equals(getType())) {
            List<Clause> tagIdClauses = getClauses(OboFormatTag.TAG_ID);
            if (tagIdClauses.size() != 1) {
                throw new FrameStructureException(this, "cardinality of id field must be 1");
            }
            // this call will verify that the value is not null
            tagIdClauses.get(0).getValue();
            if (getId() == null) {
                throw new FrameStructureException(this, "id field must be set");
            }
        }
        Collection<Clause> iClauses = getClauses(OboFormatTag.TAG_INTERSECTION_OF);
        if (iClauses.size() == 1) {
            throw new FrameStructureException(this, "single intersection_of tags are not allowed");
        }
        checkMaxOneCardinality(OboFormatTag.TAG_IS_ANONYMOUS, OboFormatTag.TAG_NAME,
            // OboFormatTag.TAG_NAMESPACE,
            OboFormatTag.TAG_DEF, OboFormatTag.TAG_COMMENT, OboFormatTag.TAG_IS_ANTI_SYMMETRIC,
            OboFormatTag.TAG_IS_CYCLIC, OboFormatTag.TAG_IS_REFLEXIVE,
            OboFormatTag.TAG_IS_SYMMETRIC, OboFormatTag.TAG_IS_TRANSITIVE,
            OboFormatTag.TAG_IS_FUNCTIONAL, OboFormatTag.TAG_IS_INVERSE_FUNCTIONAL,
            OboFormatTag.TAG_IS_OBSELETE, OboFormatTag.TAG_CREATED_BY,
            OboFormatTag.TAG_CREATION_DATE);
    }

    /**
     * Check max one cardinality.
     *
     * @param tags the tags
     * @throws FrameStructureException frame structure exception
     */
    private void checkMaxOneCardinality(OboFormatTag... tags) {
        for (OboFormatTag tag : tags) {
            if (getClauses(tag).size() > 1) {
                throw new FrameStructureException(this,
                    "multiple " + tag.getTag() + " tags not allowed.");
            }
        }
    }

    /** The Enum FrameType. */
    public enum FrameType {
        //@formatter:off
        /** HEADER. */          HEADER, 
        /** TERM. */            TERM, 
        /** TYPEDEF. */         TYPEDEF, 
        /** INSTANCE. */        INSTANCE, 
        /** ANNOTATION. */      ANNOTATION
        //@formatter:on
    }
}
