/*
 * Copyright (c) 2023, 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.
 *
 * 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.
 */

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/*

This is a simple parser for parsing the output of

   java -Xshare:dump -Xlog:cds+map=debug,cds+map+oops=trace:file=cds.map:none:filesize=0

The map file contains patterns like this for the heap objects:

======================================================================
0x00000000ffe00000: @@ Object (0xffe00000) java.lang.String
 - klass: 'java/lang/String' 0x0000000800010220
 - fields (3 words):
 - private 'hash' 'I' @12  0 (0x00000000)
 - private final 'coder' 'B' @16  0 (0x00)
 - private 'hashIsZero' 'Z' @17  true (0x01)
 - injected 'flags' 'B' @18  1 (0x01)
 - private final 'value' '[B' @20 0x00000000ffe00018 (0xffe00018) [B length: 0
0x00000000ffe00018: @@ Object (0xffe00018) [B length: 0
 - klass: {type array byte} 0x00000008000024d8
======================================================================

Currently this parser just check the output related to JDK-8308903.
I.e., each oop field must point to a valid HeapObject. For example, the 'value' field
in the String must point to a valid byte array.

This parser can be extended to check for the other parts of the map file, or perform
more analysis on the HeapObjects.

*/

public class CDSMapReader {
    public static class MapFile {
        ArrayList<HeapObject> heapObjects = new ArrayList<>();
        HashMap<Long, HeapObject> oopToObject = new HashMap<>();
        HashMap<Long, HeapObject> narrowOopToObject = new HashMap<>();
        public int stringCount = 0;

        void add(HeapObject heapObject) {
            heapObjects.add(heapObject);
            oopToObject.put(heapObject.address.oop, heapObject);
            if (heapObject.address.narrowOop != 0) {
                narrowOopToObject.put(heapObject.address.narrowOop, heapObject);
            }
            if (heapObject.className.equals("java.lang.String")) {
                stringCount ++;
            }
        }

        public int heapObjectCount() {
            return heapObjects.size();
        }
    }

    public static class HeapAddress {
        long oop;
        long narrowOop;

        HeapAddress(String oopStr, String narrowOopStr) {
            oop = Long.parseUnsignedLong(oopStr, 16);
            if (narrowOopStr != null) {
                narrowOop = Long.parseUnsignedLong(narrowOopStr, 16);
            }
        }
    }

    public static class Klass {
        long address;
        String name;

        static Klass getKlass(String name, String addr) {
            // TODO: look up from a table of known Klasses
            Klass k = new Klass();
            k.name = name;
            k.address =  Long.parseUnsignedLong(addr, 16);
            return k;
        }
    }

    public static class HeapObject {
        HeapAddress address;
        ArrayList<Field> fields;
        String className;
        Klass klass;

        HeapObject(String className, String oop, String narrowOop) {
            this.className = className;
            address = new HeapAddress(oop, narrowOop);
        }

        void setKlass(String klassName, String address) {
            klass = Klass.getKlass(klassName, address);
        }

        void addOopField(String name, String offset, String oopStr, String narrowOopStr) {
            if (fields == null) {
                fields = new ArrayList<Field>();
            }
            fields.add(new Field(name, offset, oopStr, narrowOopStr));
        }
    }

    public static class Field {
        String name;
        int offset;
        HeapAddress referentAddress; // non-null iff this is an object field
        int lineCount;

        Field(String name, String offset, String oopStr, String narrowOopStr) {
            this.name = name;
            this.offset = Integer.parseInt(offset);
            this.referentAddress = new HeapAddress(oopStr, narrowOopStr);
            this.lineCount = CDSMapReader.lineCount;
        }
    }

    // 0x00000007ffc00000:   4a5b8701 00000063 00010290 00000000 00010100 fff80003
    static Pattern rawDataPattern = Pattern.compile("^0x([0-9a-f]+): *( [0-9a-f]+)+ *$");

    // (one address)
    // 0x00000007ffc00000: @@ Object java.lang.String
    static Pattern objPattern1 = Pattern.compile("^0x([0-9a-f]+): @@ Object (.*)");

    // (two addresses)
    // 0x00000007ffc00000: @@ Object (0xfff80000) java.lang.String
    static Pattern objPattern2 = Pattern.compile("^0x([0-9a-f]+): @@ Object [(]0x([0-9a-f]+)[)] (.*)");

    //  - klass: 'java/lang/String' 0x0000000800010290
    static Pattern instanceObjKlassPattern = Pattern.compile("^ - klass: '([^']+)' 0x([0-9a-f]+)");

    //  - klass: {type array byte} 0x00000008000024c8
    static Pattern typeArrayKlassPattern = Pattern.compile("^ - klass: [{]type array ([a-z]+)[}] 0x([0-9a-f]+)");

    //  - klass: 'java/lang/Object'[] 0x00000008000013e0
    static Pattern objArrayKlassPattern = Pattern.compile("^ - klass: ('[^']+'(\\[\\])+) 0x([0-9a-f]+)");

    //  - fields (3 words):
    static Pattern fieldsWordsPattern = Pattern.compile("^ - fields [(]([0-9]+) words[)]:$");

    // (one address)
    //  - final 'key' 'Ljava/lang/Object;' @16 0x00000007ffc68260 java.lang.String
    static Pattern oopFieldPattern1 = Pattern.compile(" - [^']* '([^']+)'.*@([0-9]+) 0x([0-9a-f]+) (.*)");

    // (two addresses)
    //  - final 'key' 'Ljava/lang/Object;' @16 0x00000007ffc68260 (0xfff8d04c) java.lang.String
    static Pattern oopFieldPattern2 = Pattern.compile(" - [^']* '([^']+)'.*@([0-9]+) 0x([0-9a-f]+) [(]0x([0-9a-f]+)[)] (.*)");

    private static Matcher match(String line, Pattern pattern) {
        Matcher m = pattern.matcher(line);
        if (m.find()) {
            return m;
        } else {
            return null;
        }
    }

    private static void parseHeapObject(String className, String oop, String narrowOop) throws IOException {
        HeapObject heapObject = parseHeapObjectImpl(className, oop, narrowOop);
        mapFile.add(heapObject);
    }

    private static HeapObject parseHeapObjectImpl(String className, String oop, String narrowOop) throws IOException {
        HeapObject heapObject = new HeapObject(className, oop, narrowOop);
        Matcher m;

        nextLine();
        while (line != null && match(line, rawDataPattern) != null) { // skip raw data
            nextLine();
        }

        if (line == null || !line.startsWith(" - ")) {
            return heapObject;
        }

        if ((m = match(line, instanceObjKlassPattern)) != null) {
            heapObject.setKlass(m.group(1), m.group(2));
            nextLine();
            if ((m = match(line, fieldsWordsPattern)) == null) {
                throw new RuntimeException("Expected field size info");
            }
            while (true) {
                nextLine();
                if (line == null || !line.startsWith(" - ")) {
                    return heapObject;
                }
                if (!line.contains("marked metadata pointer")) {
                    if ((m = match(line, oopFieldPattern2)) != null) {
                        heapObject.addOopField(m.group(1), m.group(2), m.group(3), m.group(4));
                    } else if ((m = match(line, oopFieldPattern1)) != null) {
                        heapObject.addOopField(m.group(1), m.group(2), m.group(3), null);
                    }
                }
            }
        } else if ((m = match(line, typeArrayKlassPattern)) != null) {
            heapObject.setKlass(m.group(1), m.group(2));
            // TODO: read all the array elements
            while (true) {
                nextLine();
                if (line == null || !line.startsWith(" - ")) {
                    return heapObject;
                }
            }
        } else if ((m = match(line, objArrayKlassPattern)) != null) {
            heapObject.setKlass(m.group(1), m.group(3));
            // TODO: read all the array elements
            while (true) {
                nextLine();
                if (line == null || !line.startsWith(" - ")) {
                    return heapObject;
                }
            }
        } else {
            throw new RuntimeException("Expected klass info");
        }
    }

    static MapFile mapFile;
    static BufferedReader reader;
    static String line = null; // current line being parsed
    static int lineCount = 0;
    static String nextLine()  throws IOException {
        line = reader.readLine();
        ++ lineCount;
        return line;
    }

    public static MapFile read(String fileName) {
        mapFile = new MapFile();
        lineCount = 0;

        try (BufferedReader r = new BufferedReader(new FileReader(fileName))) {
            reader = r;
            nextLine();

            Matcher m;
            while (line != null) {
                if ((m = match(line, objPattern2)) != null) {
                    parseHeapObject(m.group(3), m.group(1), m.group(2));
                } else if ((m = match(line, objPattern1)) != null) {
                    parseHeapObject(m.group(2), m.group(1), null);
                } else {
                    nextLine();
                }
            }
            return mapFile;
        } catch (Throwable t) {
            System.out.println("Error parsing line " + lineCount + ": " + line);
            throw new RuntimeException(t);
        } finally {
            System.out.println("Parsed " + lineCount + " lines in " + fileName);
            System.out.println("Found " + mapFile.heapObjectCount() + " heap objects ("
                               + mapFile.stringCount + " strings)");
            mapFile = null;
            reader = null;
            line = null;
            lineCount = 0;
        }
    }

    private static void mustContain(HashMap<Long, HeapObject> allObjects, Field field, long pointer, boolean isNarrow) {
        if (allObjects.get(pointer) == null) {
            throw new RuntimeException((isNarrow ? "narrowOop" : "oop") + " pointer 0x" + Long.toHexString(pointer) +
                                       " on line " + field.lineCount + " doesn't point to a valid heap object");
        }
    }

    // Check that each oop fields in the HeapObjects must point to a valid HeapObject.
    public static void validate(MapFile mapFile) {
        int count1 = 0;
        int count2 = 0;
        for (HeapObject heapObject : mapFile.heapObjects) {
            if (heapObject.fields != null) {
                for (Field field : heapObject.fields) {
                    HeapAddress referentAddress = field.referentAddress;
                    long oop = referentAddress.oop;
                    long narrowOop = referentAddress.narrowOop;
                    // Is this test actually doing something?
                    //     To see how an invalidate pointer may be found, change oop in the
                    //     following line to oop+1
                    if (oop != 0) {
                        mustContain(mapFile.oopToObject, field, oop, false);
                        count1 ++;
                    }
                    if (narrowOop != 0) {
                        mustContain(mapFile.narrowOopToObject, field, narrowOop, true);
                        count2 ++;
                    }
                }
            }
        }
        System.out.println("Found " + count1 + " non-null oop field references (normal)");
        System.out.println("Found " + count2 + " non-null oop field references (narrow)");

        if (mapFile.heapObjectCount() > 0) {
            // heapObjectCount() may be zero if the selected GC doesn't support heap object archiving.
            if (mapFile.stringCount <= 0) {
                throw new RuntimeException("CDS map file should contain at least one string");
            }
            if (count1 < mapFile.stringCount) {
                throw new RuntimeException("CDS map file seems incorrect: " + mapFile.heapObjectCount() +
                                           " objects (" + mapFile.stringCount + " strings). Each string should" +
                                           " have one non-null oop field but we found only " + count1 +
                                           " non-null oop field references");
            }
        }
    }

    public static void main(String args[]) {
        MapFile mapFile = read(args[0]);
        validate(mapFile);
    }
}