/*
 * Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.oracle.svm.hosted.c.query;

import static com.oracle.svm.core.util.VMError.shouldNotReachHere;

import java.util.ArrayList;
import java.util.List;
import java.util.function.IntUnaryOperator;

import org.graalvm.nativeimage.c.struct.RawStructure;

import com.oracle.svm.core.config.ConfigurationValues;
import com.oracle.svm.core.util.UserError;
import com.oracle.svm.hosted.c.CInterfaceError;
import com.oracle.svm.hosted.c.NativeLibraries;
import com.oracle.svm.hosted.c.info.AccessorInfo;
import com.oracle.svm.hosted.c.info.ElementInfo;
import com.oracle.svm.hosted.c.info.NativeCodeInfo;
import com.oracle.svm.hosted.c.info.RawPointerToInfo;
import com.oracle.svm.hosted.c.info.RawStructureInfo;
import com.oracle.svm.hosted.c.info.SizableInfo.ElementKind;
import com.oracle.svm.hosted.c.info.SizableInfo.SignednessValue;
import com.oracle.svm.hosted.c.info.StructBitfieldInfo;
import com.oracle.svm.hosted.c.info.StructFieldInfo;
import com.oracle.svm.util.ReflectionUtil;
import com.oracle.svm.util.ReflectionUtil.ReflectionUtilError;

import jdk.vm.ci.meta.ResolvedJavaType;

public final class RawStructureLayoutPlanner extends NativeInfoTreeVisitor {

    private RawStructureLayoutPlanner(NativeLibraries nativeLibs) {
        super(nativeLibs);
    }

    public static void plan(NativeLibraries nativeLibs, NativeCodeInfo nativeCodeInfo) {
        /*
         * Raw structure types have no C header file. They are stored in the built-in NativeCodeInfo
         * object. We can therefore skip all the others.
         */
        if (!nativeCodeInfo.isBuiltin()) {
            return;
        }

        RawStructureLayoutPlanner planner = new RawStructureLayoutPlanner(nativeLibs);
        nativeCodeInfo.accept(planner);
    }

    @Override
    protected void visitRawStructureInfo(RawStructureInfo info) {
        if (info.isPlanned()) {
            return;
        }

        ResolvedJavaType type = info.getAnnotatedElement();
        for (ResolvedJavaType t : type.getInterfaces()) {
            if (!nativeLibs.isPointerBase(t)) {
                throw UserError.abort("Type %s must not implement %s", type, t);
            }

            if (t.equals(nativeLibs.getPointerBaseType())) {
                continue;
            }

            ElementInfo einfo = nativeLibs.findElementInfo(t);
            if (!(einfo instanceof RawStructureInfo)) {
                throw UserError.abort(new CInterfaceError("Illegal super type " + t + " found", type).getMessage());
            }

            RawStructureInfo rinfo = (RawStructureInfo) einfo;
            rinfo.accept(this);
            assert rinfo.isPlanned();

            if (info.getParentInfo() != null) {
                throw UserError.abort(new CInterfaceError("Only single inheritance of RawStructure types is supported", type).getMessage());
            }
            info.setParentInfo(rinfo);
        }

        for (ElementInfo child : new ArrayList<>(info.getChildren())) {
            if (child instanceof StructFieldInfo) {
                StructFieldInfo fieldInfo = (StructFieldInfo) child;
                StructFieldInfo parentFieldInfo = findParentFieldInfo(fieldInfo, info.getParentInfo());
                if (parentFieldInfo != null) {
                    fieldInfo.mergeChildrenAndDelete(parentFieldInfo);
                } else {
                    computeSize(fieldInfo);
                }
            }
        }

        planLayout(info);
    }

    @Override
    protected void visitRawPointerToInfo(RawPointerToInfo info) {
        info.getSizeInfo().setProperty(getSizeInBytes(info.getAnnotatedElement()));
    }

    private void computeSize(StructFieldInfo info) {
        final int declaredSize;
        if (info.isObject()) {
            declaredSize = ConfigurationValues.getObjectLayout().getReferenceSize();
        } else {
            /*
             * Resolve field size using the declared type in its accessors. Note that the field
             * offsets are not calculated before visiting all StructFieldInfos and collecting all
             * field types.
             */
            final ResolvedJavaType fieldType;
            AccessorInfo accessor = info.getAccessorInfoWithSize();
            switch (accessor.getAccessorKind()) {
                case GETTER:
                    fieldType = accessor.getReturnType();
                    break;
                case SETTER:
                    fieldType = accessor.getValueParameterType();
                    break;
                default:
                    throw shouldNotReachHere("Unexpected accessor kind " + accessor.getAccessorKind());
            }
            if (info.getKind() == ElementKind.INTEGER) {
                info.getSignednessInfo().setProperty(isSigned(fieldType) ? SignednessValue.SIGNED : SignednessValue.UNSIGNED);
            }
            declaredSize = getSizeInBytes(fieldType);
        }
        info.getSizeInfo().setProperty(declaredSize);
    }

    /**
     * Compute the offsets of each field.
     */
    private void planLayout(RawStructureInfo info) {
        /* Inherit from the parent type. */
        int currentOffset = info.getParentInfo() != null ? info.getParentInfo().getSizeInfo().getProperty() : 0;

        List<StructFieldInfo> fields = new ArrayList<>();
        for (ElementInfo child : info.getChildren()) {
            if (child instanceof StructFieldInfo) {
                fields.add((StructFieldInfo) child);
            } else if (child instanceof StructBitfieldInfo) {
                throw UserError.abort("StructBitfield is currently not supported by RawStructures!");
            }
        }

        /*
         * Sort fields in field size descending order. Note that prior to this, the fields are
         * already sorted in alphabetical order.
         */
        fields.sort((f1, f2) -> f2.getSizeInfo().getProperty() - f1.getSizeInfo().getProperty());

        for (StructFieldInfo finfo : fields) {
            assert findParentFieldInfo(finfo, info.getParentInfo()) == null;
            int fieldSize = finfo.getSizeInfo().getProperty();
            currentOffset = alignOffset(currentOffset, fieldSize);
            assert currentOffset % fieldSize == 0;
            finfo.getOffsetInfo().setProperty(currentOffset);
            currentOffset += fieldSize;
        }

        int totalSize;
        Class<? extends IntUnaryOperator> sizeProviderClass = info.getAnnotatedElement().getAnnotation(RawStructure.class).sizeProvider();
        if (sizeProviderClass == IntUnaryOperator.class) {
            /* No sizeProvider specified in the annotation, so no adjustment necessary. */
            totalSize = currentOffset;

        } else {
            IntUnaryOperator sizeProvider;
            try {
                sizeProvider = ReflectionUtil.newInstance(sizeProviderClass);
            } catch (ReflectionUtilError ex) {
                throw UserError.abort(ex.getCause(), "The size provider of @%s %s cannot be instantiated via no-argument constructor",
                                RawStructure.class.getSimpleName(), info.getAnnotatedElement().toJavaName(true));
            }

            totalSize = sizeProvider.applyAsInt(currentOffset);
            if (totalSize < currentOffset) {
                throw UserError.abort("The size provider of @%s %s computed size %d which is smaller than the minimum size of %d",
                                RawStructure.class.getSimpleName(), info.getAnnotatedElement().toJavaName(true), totalSize, currentOffset);
            }
        }

        info.getSizeInfo().setProperty(totalSize);
        info.setPlanned();
    }

    private StructFieldInfo findParentFieldInfo(StructFieldInfo fieldInfo, RawStructureInfo parentInfo) {
        if (parentInfo == null) {
            return null;
        }
        StructFieldInfo result;
        if (parentInfo.getParentInfo() != null) {
            result = findParentFieldInfo(fieldInfo, parentInfo.getParentInfo());
            if (result != null) {
                return result;
            }
        }

        for (ElementInfo child : parentInfo.getChildren()) {
            if (child instanceof StructFieldInfo) {
                StructFieldInfo parentFieldInfo = (StructFieldInfo) child;
                if (fieldInfo.getName().equals(parentFieldInfo.getName())) {
                    return parentFieldInfo;
                }
            }
        }

        return null;
    }

    private static int alignOffset(int offset, int fieldSize) {
        if (offset % fieldSize == 0) {
            return offset;
        }

        return (offset / fieldSize + 1) * fieldSize;
    }
}
