/*
 * Copyright (c) 2013, 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.shouldNotReachHereUnexpectedInput;
import static com.oracle.svm.hosted.c.query.QueryParserUtil.parseHexToLong;
import static com.oracle.svm.hosted.c.query.QueryParserUtil.parseSigned;
import static com.oracle.svm.hosted.c.query.QueryParserUtil.unsignedExtendToSize;

import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.graalvm.nativeimage.c.type.CCharPointer;

import com.oracle.graal.pointsto.meta.AnalysisType;
import com.oracle.svm.hosted.c.NativeLibraries;
import com.oracle.svm.hosted.c.info.AccessorInfo;
import com.oracle.svm.hosted.c.info.ConstantInfo;
import com.oracle.svm.hosted.c.info.ElementInfo;
import com.oracle.svm.hosted.c.info.EnumConstantInfo;
import com.oracle.svm.hosted.c.info.NativeCodeInfo;
import com.oracle.svm.hosted.c.info.PointerToInfo;
import com.oracle.svm.hosted.c.info.PropertyInfo;
import com.oracle.svm.hosted.c.info.RawPointerToInfo;
import com.oracle.svm.hosted.c.info.RawStructureInfo;
import com.oracle.svm.hosted.c.info.SizableInfo;
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.hosted.c.info.StructInfo;
import com.oracle.svm.hosted.c.util.FileUtils;

import jdk.vm.ci.meta.JavaKind;

/**
 * Parses query result described in {@link QueryResultFormat}.
 */
public final class QueryResultParser extends NativeInfoTreeVisitor {

    private final Map<String, String> idToResult;

    private QueryResultParser(NativeLibraries nativeLibs) {
        super(nativeLibs);
        this.idToResult = new HashMap<>();
    }

    public static List<String> parse(NativeLibraries nativeLibs, NativeCodeInfo nativeCodeInfo, InputStream source) {
        QueryResultParser parser = new QueryResultParser(nativeLibs);
        List<String> lines = FileUtils.readAllLines(source);
        for (String line : lines) {
            String[] keyValuePair = line.split(QueryResultFormat.DELIMINATOR);
            assert keyValuePair.length == 2;
            parser.idToResult.put(keyValuePair[0], keyValuePair[1]);
        }

        nativeCodeInfo.accept(parser);
        return lines;
    }

    @Override
    protected void visitConstantInfo(ConstantInfo constantInfo) {
        switch (constantInfo.getKind()) {
            case INTEGER:
                parseIntegerProperty(constantInfo.getSizeInfo());
                parseSignedness(constantInfo.getSignednessInfo());
                parseIntegerConstantValue(constantInfo.getValueInfo());

                /*
                 * From the point of view of the C compiler, plain #define constants have the type
                 * int and therefore size 4. But sometimes we want to access such values as short or
                 * byte to avoid casts. Check the actual value of the constant, and if it fits the
                 * declared type of the constant, then change the actual size to the declared size.
                 */
                JavaKind returnKind = AccessorInfo.getReturnType(constantInfo.getAnnotatedElement()).getJavaKind();
                if (returnKind == JavaKind.Object) {
                    returnKind = nativeLibs.getTarget().wordJavaKind;
                }
                int declaredSize = getSizeInBytes(returnKind);
                int actualSize = constantInfo.getSizeInfo().getProperty();
                if (declaredSize != actualSize) {
                    long value = (long) constantInfo.getValueInfo().getProperty();
                    if (value >= returnKind.getMinValue() && value <= returnKind.getMaxValue()) {
                        constantInfo.getSizeInfo().setProperty(declaredSize);
                    }
                }

                break;
            case POINTER:
                parseIntegerProperty(constantInfo.getSizeInfo());
                parseIntegerConstantValue(constantInfo.getValueInfo());
                break;
            case FLOAT:
                parseIntegerProperty(constantInfo.getSizeInfo());
                parseFloatValue(constantInfo.getValueInfo());
                break;
            case STRING:
                parseStringValue(constantInfo.getValueInfo());
                break;
            case BYTEARRAY:
                parseByteArrayValue(constantInfo.getValueInfo());
                break;
            default:
                throw shouldNotReachHereUnexpectedInput(constantInfo.getKind()); // ExcludeFromJacocoGeneratedReport
        }
    }

    @Override
    public void visitStructInfo(StructInfo structInfo) {
        if (!structInfo.isIncomplete()) {
            parseIntegerProperty(structInfo.getSizeInfo());
        }
        processChildren(structInfo);
    }

    @Override
    protected void visitRawStructureInfo(RawStructureInfo info) {
        /* Nothing to do, do not visit children. */
    }

    @Override
    public void visitStructFieldInfo(StructFieldInfo fieldInfo) {
        parseIntegerProperty(fieldInfo.getSizeInfo());
        parseIntegerProperty(fieldInfo.getOffsetInfo());

        if (fieldInfo.getKind() == ElementKind.INTEGER) {
            parseSignedness(fieldInfo.getSignednessInfo());
        }
    }

    @Override
    public void visitStructBitfieldInfo(StructBitfieldInfo bitfieldInfo) {
        parseIntegerProperty(bitfieldInfo.getByteOffsetInfo());
        parseIntegerProperty(bitfieldInfo.getStartBitInfo());
        parseIntegerProperty(bitfieldInfo.getEndBitInfo());
        parseSignedness(bitfieldInfo.getSignednessInfo());
    }

    @Override
    public void visitPointerToInfo(PointerToInfo pointerToInfo) {
        parseIntegerProperty(pointerToInfo.getSizeInfo());

        if (pointerToInfo.getKind() == ElementKind.INTEGER) {
            if (((AnalysisType) pointerToInfo.getAnnotatedElement()).getJavaClass() == CCharPointer.class) {
                /* Always treat CCharPointer as signed internally (Java byte read/writes). */
                pointerToInfo.getSignednessInfo().setProperty(SignednessValue.SIGNED);
            } else {
                parseSignedness(pointerToInfo.getSignednessInfo());
            }
        }
    }

    @Override
    public void visitRawPointerToInfo(RawPointerToInfo pointerToInfo) {
        /* Nothing to do, do not visit children. */
    }

    @Override
    protected void visitEnumConstantInfo(EnumConstantInfo constantInfo) {
        assert constantInfo.getKind() == ElementKind.INTEGER;
        parseIntegerProperty(constantInfo.getSizeInfo());
        parseSignedness(constantInfo.getSignednessInfo());
        parseIntegerConstantValue(constantInfo.getValueInfo());
    }

    private void parseSignedness(PropertyInfo<SignednessValue> info) {
        info.setProperty(SignednessValue.valueOf(stringLiteral(info)));
    }

    private void parseIntegerConstantValue(PropertyInfo<Object> info) {
        boolean isUnsigned = ((SizableInfo) info.getParent()).isUnsigned();
        int size = ((SizableInfo) info.getParent()).getSizeInfo().getProperty();
        String hex = idToResult.get(info.getUniqueID());
        int hexSize = hex.length() / 2;

        if (hexSize < size) {
            hex = unsignedExtendToSize(size, hex);
        }

        if (isUnsigned) {
            parseHexToLong(info, hex);
        } else {
            parseSigned(info, hex);
        }
    }

    private void parseFloatValue(PropertyInfo<Object> info) {
        String str = idToResult.get(info.getUniqueID());
        double value = Double.parseDouble(str);
        info.setProperty(value);
    }

    private void parseStringValue(PropertyInfo<Object> info) {
        info.setProperty(stringLiteral(info));
    }

    private String stringLiteral(ElementInfo info) {
        String str = idToResult.get(info.getUniqueID());
        if (str.startsWith(QueryResultFormat.STRING_MARKER) && str.endsWith(QueryResultFormat.STRING_MARKER)) {
            return str.substring(QueryResultFormat.STRING_MARKER.length(), str.length() - QueryResultFormat.STRING_MARKER.length());
        } else {
            addError("String constant not deliminated correctly", info);
            return "";
        }
    }

    private void parseByteArrayValue(PropertyInfo<Object> info) {
        info.setProperty(byteArrayLiteral(info));
    }

    private byte[] byteArrayLiteral(ElementInfo info) {
        String str = stringLiteral(info);
        if (!str.isEmpty()) {
            return str.getBytes(Charset.forName("UTF-8"));
        } else {
            return new byte[0];
        }
    }

    private void parseIntegerProperty(PropertyInfo<Integer> info) {
        int value = Integer.parseInt(idToResult.get(info.getUniqueID()));
        info.setProperty(value);
    }
}
