/*******************************************************************************
 * Copyright (c) 2014 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
 *******************************************************************************/

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

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;

import java.util.Collection;
import java.util.List;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
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;
import org.eclipse.tracecompass.ctf.core.event.types.AbstractArrayDefinition;
import org.eclipse.tracecompass.ctf.core.event.types.CompoundDeclaration;
import org.eclipse.tracecompass.ctf.core.event.types.Definition;
import org.eclipse.tracecompass.ctf.core.event.types.IDeclaration;
import org.eclipse.tracecompass.ctf.core.event.types.IDefinition;
import org.eclipse.tracecompass.ctf.core.event.types.IntegerDefinition;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.Multimap;

/**
 * A CTF sequence declaration.
 *
 * An array where the size is fixed but declared in the trace, unlike array
 * where it is declared with a literal
 *
 * @author Matthew Khouzam
 */
public class SequenceDeclaration extends CompoundDeclaration {

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

    private final IDeclaration fElemType;
    private final String fLengthName;
    private final transient Multimap<String, String> fPaths = ArrayListMultimap.create();

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

    /**
     * Constructor
     *
     * @param lengthName
     *            the name of the field describing the length
     * @param elemType
     *            The element type
     */
    public SequenceDeclaration(@Nullable String lengthName, IDeclaration elemType) {
        fElemType = elemType;
        fLengthName = lengthName;
    }

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

    @Override
    public IDeclaration getElementType() {
        return fElemType;
    }

    /**
     * Gets the name of the length field
     *
     * @return the name of the length field
     */
    public String getLengthName() {
        return fLengthName;
    }

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

    @Override
    public AbstractArrayDefinition createDefinition(
            @Nullable IDefinitionScope definitionScope, String fieldName, BitBuffer input) throws CTFException {
        IDefinition lenDef = null;

        if (definitionScope != null) {
            lenDef = definitionScope.lookupDefinition(getLengthName());
        }

        if (lenDef == null) {
            throw new CTFException("Sequence length field not found"); //$NON-NLS-1$
        }

        if (!(lenDef instanceof IntegerDefinition)) {
            throw new CTFException("Sequence length field not integer"); //$NON-NLS-1$
        }

        IntegerDefinition lengthDefinition = (IntegerDefinition) lenDef;

        if (lengthDefinition.getDeclaration().isSigned()) {
            throw new CTFException("Sequence length must not be signed"); //$NON-NLS-1$
        }

        long length = lengthDefinition.getValue();
        long maxBits = length * fElemType.getMaximumSize();
        if ((length > Integer.MAX_VALUE) || (maxBits > Integer.MAX_VALUE) || (!input.canRead((int) maxBits))) {
            throw new CTFException("Sequence length too long " + length); //$NON-NLS-1$
        }

        if (isAlignedBytes()) {
            // Don't create "useless" definitions
            byte[] data = new byte[(int) length];
            input.get(data);
            return new ByteArrayDefinition(this, definitionScope, fieldName, data);
        }
        Collection<String> collection = fPaths.get(fieldName);
        while (collection.size() < length) {
            fPaths.put(fieldName, fieldName + '[' + collection.size() + ']');
        }
        List<String> paths = (List<String>) fPaths.get(fieldName);
        Builder<@NonNull Definition> definitions = new ImmutableList.Builder<>();
        for (int i = 0; i < length; i++) {
            /* We should not have inserted any null values */
            String elemName = checkNotNull(paths.get(i));
            definitions.add(fElemType.createDefinition(definitionScope, elemName, input));
        }
        List<@NonNull Definition> list = definitions.build();
        return new ArrayDefinition(this, definitionScope, fieldName, list);
    }

    @Override
    public String toString() {
        /* Only used for debugging */
        return "[declaration] sequence[" + Integer.toHexString(hashCode()) + ']'; //$NON-NLS-1$
    }

    @Override
    public int getMaximumSize() {
        return Integer.MAX_VALUE;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + fElemType.hashCode();
        result = prime * result + fLengthName.hashCode();
        return result;
    }

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

    @Override
    public boolean isBinaryEquivalent(@Nullable IDeclaration obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        SequenceDeclaration other = (SequenceDeclaration) obj;
        if (!fElemType.isBinaryEquivalent(other.fElemType)) {
            return false;
        }
        return (fLengthName.equals(other.fLengthName));
    }

}