/*******************************************************************************
 * Copyright (c) 2009, 2018 STMicroelectronics and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Xavier Raynaud <xavier.raynaud@st.com> - initial API and implementation
 *******************************************************************************/
package org.eclipse.linuxtools.internal.gcov.parser;

import java.io.DataInput;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.linuxtools.internal.gcov.Activator;
import org.eclipse.linuxtools.internal.gcov.utils.BEDataInputStream;
import org.eclipse.linuxtools.internal.gcov.utils.GcovStringReader;
import org.eclipse.linuxtools.internal.gcov.utils.LEDataInputStream;
import org.eclipse.linuxtools.internal.gcov.utils.MasksGenerator;
import org.eclipse.osgi.util.NLS;

public class GcnoRecordsParser {

    private static final int GCOV_NOTE_MAGIC = 0x67636e6f; // en ASCII: 67=g 63=c 6e=n 6f=o
    private static final int GCOV_TAG_FUNCTION = 0x01000000;
    private static final int GCOV_TAG_BLOCKS = 0x01410000;
    private static final int GCOV_TAG_ARCS = 0x01430000;
    private static final int GCOV_TAG_LINES = 0x01450000;

    private static final int GCC_VER_810 = 1094201642; // GCC 8.1.0 ('A81*')
	private static final int GCC_VER_910 = 1094267178; // GCC 9.1.0 ('A91*')
    private static final int GCC_VER_407 = 875575082; // GCC 4.0.7

    private GcnoFunction fnctn = null;
    private final ArrayList<GcnoFunction> fnctns = new ArrayList<>();
    private final ArrayList<SourceFile> currentAllSrcs;
    private final HashMap<String, SourceFile> sourceMap;

    public GcnoRecordsParser(HashMap<String, SourceFile> sourceMap, ArrayList<SourceFile> allSrcs) {
        this.sourceMap = sourceMap;
        this.currentAllSrcs = allSrcs;
    }

    private SourceFile findOrAdd(String fileName) {
        SourceFile newsrc = sourceMap.get(fileName);
        if (newsrc == null) {
            newsrc = new SourceFile(fileName, currentAllSrcs.size() + 1);
            currentAllSrcs.add(newsrc);
            sourceMap.put(fileName, newsrc);
        }
        return newsrc; // return the new added element
    }

    public void parseData(DataInput stream) throws IOException, CoreException {
        // header data
        int magic = 0;
        // blocks data
        ArrayList<Block> blocks = null;
        // source file data
        SourceFile source = null;
        // flag
        boolean parseFirstFnctn = false;

        magic = stream.readInt();
        if (magic == GCOV_NOTE_MAGIC) {
            stream = new BEDataInputStream((DataInputStream) stream);
        } else {
            magic = (magic >> 16) | (magic << 16);
            magic = ((magic & 0xff00ff) << 8) | ((magic >> 8) & 0xff00ff);
            if (magic == GCOV_NOTE_MAGIC) {
                stream = new LEDataInputStream((DataInputStream) stream);
            } else {
                String message = NLS.bind(Messages.GcnoRecordsParser_magic_num_error, magic);
                Status status = new Status(IStatus.ERROR, Activator.PLUGIN_ID, message);
                throw new CoreException(status);
            }
        }

        int version = stream.readInt();
        // stamp = stream.readInt();
        stream.readInt();

        /*------------------------------------------------------------------------------
        System.out.println("Gcno LE, Magic "+magic+" version "+version+" stamp "+stamp);
         */

        while (true) {
            try {
                int tag;
                // parse header
                while (true) {
                    tag = stream.readInt();
                    if (tag == GCOV_TAG_FUNCTION || tag == GCOV_TAG_BLOCKS || tag == GCOV_TAG_ARCS
                            || tag == GCOV_TAG_LINES)
                        break;
                }
                int length = stream.readInt();

                // parse gcno data
                if (tag == GCOV_TAG_FUNCTION) {
                    // before parse new function, add current function to functions list
                    if (parseFirstFnctn) {
                        fnctns.add(fnctn);
                    }

                    long fnctnIdent = (stream.readInt() & MasksGenerator.UNSIGNED_INT_MASK);
                    long fnctnChksm = (stream.readInt() & MasksGenerator.UNSIGNED_INT_MASK);
                    /*
                     * danielhb, 2012-08-06: Gcov versions 4.7.0 or later (long value = 875575082) has different format
                     * for the data file: prior format: announce_function: header int32:ident int32:checksum new format:
                     * announce_function: header int32:ident int32:lineno_checksum int32:cfg_checksum TL;DR Need to
                     * consume the extra long value.
                     */
                    if (version >= GCC_VER_407) {
                        // long cfgChksm = (stream.readInt()&MasksGenerator.UNSIGNED_INT_MASK);
                        stream.readInt();
                    }
                    String fnctnName = GcovStringReader.readString(stream);
                    if (version >= GCC_VER_810) {
                        // long artificial = (stream.readInt() & MasksGenerator.UNSIGNED_INT_MASK);
                        stream.readInt();
                    }
                    String fnctnSrcFle = GcovStringReader.readString(stream);
                    long fnctnFrstLnNmbr = (stream.readInt() & MasksGenerator.UNSIGNED_INT_MASK);
                    if (version >= GCC_VER_810) {
						// long fnctnFrstColumnNmbr = (stream.readInt() &
						// MasksGenerator.UNSIGNED_INT_MASK);
                        stream.readInt();
                        // long fnctnLastLnNmbr = (stream.readInt() & MasksGenerator.UNSIGNED_INT_MASK);
                        stream.readInt();
                    }

					if (version >= GCC_VER_910) {
						// long fnctnLastColumnNmbr = (stream.readInt() &
						// MasksGenerator.UNSIGNED_INT_MASK);
						stream.readInt();
					}

                    fnctn = new GcnoFunction(fnctnIdent, fnctnChksm, fnctnName, fnctnSrcFle, fnctnFrstLnNmbr);
                    SourceFile srcFle2 = findOrAdd(fnctn.getSrcFile());
                    if (fnctn.getFirstLineNmbr() >= srcFle2.getNumLines()) {
                        srcFle2.setNumLines((int) fnctn.getFirstLineNmbr() + 1);
                    }
                    srcFle2.addFnctn(fnctn);
                    parseFirstFnctn = true;
                    continue;
                }

                else if (tag == GCOV_TAG_BLOCKS) {
                    if (version >= GCC_VER_810) {
                        length = stream.readInt();
                    }
                    blocks = new ArrayList<>();
                    Block blck;
                    for (int i = 0; i < length; i++) {
                        if (version >= GCC_VER_810) {
                            blck = new Block(0); // value not used anywhere
                        } else {
                            long BlckFlag = stream.readInt() & MasksGenerator.UNSIGNED_INT_MASK;
                            blck = new Block(BlckFlag);
                        }
                        blocks.add(blck);
                    }
                    fnctn.setNumBlocks(length);
                    continue;
                } else if (tag == GCOV_TAG_ARCS) {
                    int srcBlockIndice = stream.readInt();
                    int nmbrArcs = (length - 1) / 2;
                    ArrayList<Arc> arcs = new ArrayList<>(nmbrArcs);

                    for (int i = 0; i < nmbrArcs; i++) {
                        int dstnatnBlockIndice = stream.readInt();
                        long flag = (stream.readInt() & MasksGenerator.UNSIGNED_INT_MASK);
                        Arc arc = new Arc(srcBlockIndice, dstnatnBlockIndice, flag, blocks);
                        arcs.add(arc);
                    }

                    // each arc, register it as exit of the src block
                    Block srcBlk = blocks.get(srcBlockIndice);
                    for (Arc a : arcs) {
                        srcBlk.addExitArcs(a);
                        srcBlk.incNumSuccs();
                    }

                    // each arc, register it as entry of its dstntn block
                    for (Arc a : arcs) {
                        Block dstntnBlk = a.getDstnatnBlock();
                        dstntnBlk.addEntryArcs(a);
                        dstntnBlk.incNumPreds();
                    }

                    for (Arc a : arcs) {
                        if (a.isFake()) {
                            if (a.getSrcBlock() != null) {
                                // Exceptional exit from this function, the
                                // source block must be a call.
                                srcBlk = blocks.get(srcBlockIndice);
                                srcBlk.setCallSite(true);
                                a.setCallNonReturn(true);
                            } else {
                                a.setNonLoclaReturn(true);
                                Block dstntnBlk = a.getDstnatnBlock();
                                dstntnBlk.setNonLocalReturn(true);
                            }
                        }

                        if (!a.isOnTree()) {
                            fnctn.incNumCounts();
                        }
                        // nbrCounts++;
                    }

                    fnctn.setFunctionBlocks(blocks);
                    continue;
                }

                else if (tag == GCOV_TAG_LINES) {
                    int numBlock = stream.readInt();
                    long[] lineNos = new long[length - 1];
                    int ix = 0;
                    do {
                        long lineNumber = stream.readInt() & MasksGenerator.UNSIGNED_INT_MASK;
                        if (lineNumber != 0) {
                            if (ix == 0) {
                                lineNos[ix++] = 0;
                                lineNos[ix++] = source.getIndex();
                            }
                            lineNos[ix++] = lineNumber;
                            if (lineNumber >= source.getNumLines()) {
                                source.setNumLines((int) lineNumber + 1);
                            }
                        } else {
                            String fileName = GcovStringReader.readString(stream);
                            if (fileName.equals(Messages.GcnoRecordsParser_null_string)){
                                break;
                            }

                            source = findOrAdd(fileName);
                            lineNos[ix++] = 0;
                            lineNos[ix++] = source.getIndex();
                        }
                    } while (true);

                    fnctn.getFunctionBlocks().get((numBlock)).setEncoding(lineNos);
                    fnctn.getFunctionBlocks().get((numBlock)).setNumLine(ix);
                    continue;
                }
            } catch (EOFException e) {
                fnctn.setFunctionBlocks(blocks);
                fnctns.add(fnctn);
                break;
            }
        }// while
    }

    /* Getters */
    public ArrayList<GcnoFunction> getFnctns() {
        return fnctns;
    }
}
