/*******************************************************************************
 * Copyright (c) 2011, 2015 Ericsson, Ecole Polytechnique de Montreal and others
 *
 * All rights reserved. This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors: Matthew Khouzam - Initial API and implementation
 * Contributors: Simon Marchi - Initial API and implementation
 *******************************************************************************/

package org.eclipse.tracecompass.ctf.core.event.types;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

import org.eclipse.tracecompass.ctf.core.CTFException;
import org.eclipse.tracecompass.ctf.core.event.io.BitBuffer;
import org.eclipse.tracecompass.ctf.core.event.scope.IDefinitionScope;

/**
 * A CTF C variant declaration.
 *
 * A variant is similar to a C union, only taking the minimum size of the types,
 * it is a compound data type that contains other datatypes in fields. they are
 * stored in an hashmap and indexed by names which are strings.
 *
 * @version 1.0
 * @author Matthew Khouzam
 * @author Simon Marchi
 */
public class VariantDeclaration extends Declaration {

    // ------------------------------------------------------------------------
    // Attributes
    // ------------------------------------------------------------------------

    private String fTag = null;
    private static final long ALIGNMENT = 1;
    private final Map<String, IDeclaration> fFields = Collections.synchronizedMap(new HashMap<String, IDeclaration>());
    private IDeclaration fDeclarationToPopulate;

    // ------------------------------------------------------------------------
    // Constructors
    // ------------------------------------------------------------------------

    /**
     * Constructor
     */
    public VariantDeclaration() {
        // Do nothing
    }

    // ------------------------------------------------------------------------
    // Getters/Setters/Predicates
    // ------------------------------------------------------------------------

    /**
     * @return Does the variant have a tag
     */
    public boolean isTagged() {
        return fTag != null;
    }

    /**
     * Lookup if a field exists in the variant
     *
     * @param fieldTag
     *            the field tag name
     * @return true = field tag exists
     */
    public boolean hasField(String fieldTag) {
        return fFields.containsKey(fieldTag);
    }

    /**
     * Sets the tag in a variant
     *
     * @param tag
     *            the tag
     */
    public void setTag(String tag) {
        fTag = tag;
    }

    /**
     * Gets current variant tag
     *
     * @return the variant tag.
     */
    public String getTag() {
        return fTag;
    }

    /**
     * Gets the fields of the variant
     *
     * @return the fields of the variant
     */
    public Map<String, IDeclaration> getFields() {
        return this.fFields;
    }

    @Override
    public long getAlignment() {
        return ALIGNMENT;
    }

    // ------------------------------------------------------------------------
    // Operations
    // ------------------------------------------------------------------------

    @Override
    public VariantDefinition createDefinition(IDefinitionScope definitionScope,
            String fieldName, BitBuffer input) throws CTFException {
        alignRead(input);
        IDefinition def = definitionScope.lookupDefinition(fTag);
        EnumDefinition tagDef = (EnumDefinition) ((def instanceof EnumDefinition) ? def : null);
        if (tagDef == null) {
            throw new CTFException("Tag is not defined " + fTag); //$NON-NLS-1$
        }
        String varFieldName = tagDef.getStringValue();
        if (varFieldName == null) {
            throw new CTFException("Undefined enum selector for variant " + //$NON-NLS-1$
                    definitionScope.getScopePath().getPath());
        }
        fDeclarationToPopulate = fFields.get(varFieldName);
        if (fDeclarationToPopulate == null) {
            throw new CTFException("Unknown enum selector for variant " + //$NON-NLS-1$
                    definitionScope.getScopePath().getPath());
        }
        Definition fieldValue = fDeclarationToPopulate.createDefinition(definitionScope, fieldName, input);
        return new VariantDefinition(this, definitionScope, tagDef, varFieldName, fieldName, fieldValue);
    }

    /**
     * Add a field to this CTF Variant
     *
     * @param fieldTag
     *            The tag of the new field
     * @param declaration
     *            The Declaration of this new field
     */
    public void addField(String fieldTag, IDeclaration declaration) {
        fFields.put(fieldTag, declaration);
    }

    @Override
    public int getMaximumSize() {
        Collection<IDeclaration> values = fFields.values();
        int maxSize = 0;
        for (IDeclaration field : values) {
            maxSize = Math.max(maxSize, field.getMaximumSize());
        }
        return maxSize;
    }

    @Override
    public String toString() {
        /* Only used for debugging */
        StringBuilder sb = new StringBuilder();
        sb.append("[declaration] variant["); //$NON-NLS-1$
        for (Entry<String, IDeclaration> field : fFields.entrySet()) {
            sb.append(field.getKey()).append(':').append(field.getValue());
        }
        sb.append(']');
        return sb.toString();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((fDeclarationToPopulate == null) ? 0 : fDeclarationToPopulate.hashCode());
        if (fFields == null) {
            result = prime * result;
        } else {
            for (Entry<String, IDeclaration> field : fFields.entrySet()) {
                result = prime * result + field.getValue().hashCode();
            }
        }
        result = prime * result + ((fTag == null) ? 0 : fTag.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        VariantDeclaration other = (VariantDeclaration) obj;

        if (!Objects.equals(fDeclarationToPopulate, other.fDeclarationToPopulate)) {
            return false;
        }
        // do not check the order of the fields
        if (!Objects.equals(fFields, other.fFields)) {
            return false;
        }
        return (Objects.equals(fTag, other.fTag));
    }

    @Override
    public boolean isBinaryEquivalent(IDeclaration obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        VariantDeclaration other = (VariantDeclaration) obj;
        if (fFields == null) {
            if (other.fFields != null) {
                return false;
            }
        } else {
            if (fFields.size() != other.fFields.size()) {
                return false;
            }
            for (Entry<String, IDeclaration> field : fFields.entrySet()) {
                if (!other.fFields.containsKey(field.getKey())) {
                    return false;
                }
                IDeclaration field2 = other.fFields.get(field.getKey());
                if ((field2 == null) || (!field2.isBinaryEquivalent(field.getValue()))) {
                    return false;
                }
            }
        }
        return true;
    }

}
