/*
 * Copyright (c) 2010, 2012, 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.
 */
package com.sun.max.tele.debug.darwin;

import java.io.*;
import java.nio.*;


/**
 * (Limited) Access to Mach_O 64-bit format files. See /usr/include/mach-o/*.h for source.
 * Note that a file may (unusually) contain multiple binaries for different architectures,
 * see /usr/include/mach-o/fat.h. Such a file is called a universal binary file, (cf an archive file).
 *
 */
public class DarwinMachO {

    /**
     * Encapsulates whether the MachO file is embedded in a universal binary file.
     */
    public static class MachORandomAccessFile extends RandomAccessFile {

        /**
         * If non-zero, offset to start of architecture-specific MachO file in FAT file.
         */
        private int fatOffset;

        private MachORandomAccessFile(String path) throws FileNotFoundException {
            super(path, "r");
        }

        @Override
        public void seek(long filePos) throws IOException {
            super.seek(filePos + fatOffset);
        }

        @Override
        public long getFilePointer() throws IOException {
            long fp = super.getFilePointer();
            return fp - fatOffset;
        }
    }

    public final MachORandomAccessFile raf;
    private Header header;
    private LoadCommand[] loadCommands;

    public DarwinMachO(String path) throws FileNotFoundException, IOException {
        raf = new MachORandomAccessFile(path);
        header = new Header();
    }

    private class FatArch {
        final static int X86_64 = 0x01000007;
        int cputype;
        int cpusubtype;
        int offset;
        int size;
        int align;

        private FatArch() throws IOException {
            cputype = raf.readInt();
            cpusubtype = raf.readInt();
            offset = raf.readInt();
            size = raf.readInt();
            align = raf.readInt();
        }
    }

    public class Header {
        private static final int FAT_MAGIC = 0xcafebabe;
        public final int magic;
        public final int cputype;
        public final int cpusubtype;
        public final int filetype;
        public final int ncmds;
        public final int sizeofcmds;
        public final int flags;
        public final int reserved;

        private Header() throws IOException {
            int magic = raf.readInt();
            boolean found = false;
            if (magic == FAT_MAGIC) {
                // find our architecture subfile
                int nFatArch = raf.readInt();
                for (int i = 0; i < nFatArch; i++) {
                    FatArch fatArch = new FatArch();
                    if (fatArch.cputype == FatArch.X86_64) {
                        found = true;
                        raf.seek(fatArch.offset);
                        raf.fatOffset = fatArch.offset;
                        magic = readInt();
                        break;
                    }
                }
                assert found : "failed to find X86_64 architecture in MachO FAT file";
            }
            this.magic = magic;
            cputype = readInt();
            cpusubtype = readInt();
            filetype = readInt();
            ncmds = readInt();
            sizeofcmds = readInt();
            flags = readInt();
            reserved = readInt();

        }

    }

    public Header getHeader() throws IOException {
        if (header == null) {
            header = new Header();
        }
        return header;
    }

    /**
     * Common base class for all Mach-O load command types.
     */
    public class LoadCommand {
        public static final int LC_SEGMENT_64 = 0x19;
        public static final int LC_THREAD = 0x4;
        public static final int LC_SYMTAB = 0x2;

        public final int cmd;
        public final int cmdsize;

        protected LoadCommand() throws IOException {
            this.cmd = readInt();
            this.cmdsize = readInt();
        }

        public String typeName() {
            switch (cmd) {
                case LC_SEGMENT_64:
                    return "LC_SEGMENT_64";
                case LC_THREAD:
                    return "LC_THREAD";
                case LC_SYMTAB:
                    return "LC_SYMTAB";
                default:
                    return "LC #" + cmd;
            }
        }

    }

    /**
     * Reads a load command structure starting at the current file position, invoking
     * the appropriate subclass {@code read} command, based on the {@code cmd} field.
     * Leaves the file pointer at the next load command (if any).
     *
     * @return instance of the appropriate subclass for discovered command type
     * @throws IOException
     */
    private LoadCommand readNextLoadCommand() throws IOException {
        LoadCommand result = null;
        final long ptr = raf.getFilePointer();
        final int cmd = readInt();
        final int cmdsize = readInt();
        raf.seek(ptr);
        switch (cmd) {
            case LoadCommand.LC_SEGMENT_64:
                result = new Segment64LoadCommand();
                break;
            case LoadCommand.LC_THREAD:
                result = new ThreadLoadCommand();
                break;
            case LoadCommand.LC_SYMTAB:
                result = new SymTabLoadCommand();
                break;
            default:
                result = new LoadCommand();
        }
        // skip over entire command
        raf.seek(ptr + cmdsize);
        return result;
    }

    public LoadCommand[] getLoadCommands() throws IOException {
        if (loadCommands == null) {
            getHeader();
            loadCommands = new LoadCommand[header.ncmds];
            for (int i = 0; i < header.ncmds; i++) {
                loadCommands[i] = readNextLoadCommand();
            }
        }
        return loadCommands;
    }

    public final class Segment64LoadCommand extends LoadCommand {
        public final String segName;
        public final long vmaddr;
        public final long vmsize;
        public final long fileoff;
        public final long filesize;
        public final int maxprot;
        public final int initprot;
        public final int nsects;
        public final int flags;
        public final Section64[] sections;

        private Segment64LoadCommand() throws IOException {
            final byte[] segname = new byte[16];
            for (int i = 0; i < 16; i++) {
                segname[i] = raf.readByte();
            }
            segName = new String(segname);
            vmaddr = readLong();
            vmsize = readLong();
            fileoff = readLong();
            filesize = readLong();
            maxprot = readInt();
            initprot = readInt();
            nsects = readInt();
            flags = readInt();
            sections = new Section64[nsects];
            for (int i = 0; i < nsects; i++) {
                sections[i] = new Section64(this);
            }
        }

    }

    public class Section64 {
        public final String sectname;
        public final String segname;
        public final long addr;
        public final long size;
        public final int offset;
        public final int align;
        public final int reloff;
        public final int nreloc;
        public final int flags;
        public final int reserved1;
        public final int reserved2;
        public final int reserved3;
        public final Segment64LoadCommand owningSegment;

        private Section64(Segment64LoadCommand segment64) throws IOException {
            owningSegment = segment64;
            sectname = readName();
            segname = readName();
            addr = readLong();
            size = readLong();
            offset = readInt();
            align = readInt();
            reloff = readInt();
            nreloc = readInt();
            flags = readInt();
            reserved1 = readInt();
            reserved2 = readInt();
            reserved3 = readInt();
        }

        private String readName() throws IOException {
            byte[] nameBytes = new byte[16];
            int length = 0;
            for (int i = 0; i < nameBytes.length; i++) {
                nameBytes[i] = raf.readByte();
                if (nameBytes[i] != 0) {
                    length++;
                }
            }
            return new String(nameBytes, 0, length);
        }

        public boolean isText() {
            return segname.equals("__TEXT");
        }
    }

    public class ThreadLoadCommand extends LoadCommand {
        public final ThreadRegState regstate;
        public final ThreadFPRegState fpregstate;
        public final ThreadExceptionState exstate;

        ThreadLoadCommand() throws IOException {
            regstate = new ThreadRegState();
            fpregstate = new ThreadFPRegState();
            exstate = new ThreadExceptionState();
        }

    }

    public class ThreadState {
        public final int flavor;
        public final int count;

        protected ThreadState() throws IOException {
            flavor = readInt();
            count = readInt();
        }
    }

    public class ThreadRegState extends ThreadState {
        public final int tsh_flavor;
        public final int tsh_count;
        public final ByteBuffer regbytes; // we also store registers as a directly allocated ByteBuffer for passing to native code
        public final long      rax;
        public final long      rbx;
        public final long      rcx;
        public final long      rdx;
        public final long      rdi;
        public final long      rsi;
        public final long      rbp;
        public final long      rsp;
        public final long      r8;
        public final long      r9;
        public final long      r10;
        public final long      r11;
        public final long      r12;
        public final long      r13;
        public final long      r14;
        public final long      r15;
        public final long      rip;
        public final long      rflags;
        public final long      cs;
        public final long      fs;
        public final long      gs;

        ThreadRegState() throws IOException {
            tsh_flavor = readInt();
            tsh_count = readInt();
            final long ptr = raf.getFilePointer();
            rax = readLong();
            rbx = readLong();
            rcx = readLong();
            rdx = readLong();
            rdi = readLong();
            rsi = readLong();
            rbp = readLong();
            rsp = readLong();
            r8 = readLong();
            r9 = readLong();
            r10 = readLong();
            r11 = readLong();
            r12 = readLong();
            r13 = readLong();
            r14 = readLong();
            r15 = readLong();
            rip = readLong();
            rflags = readLong();
            cs = readLong();
            fs = readLong();
            gs = readLong();
            raf.seek(ptr);
            // read and store again as byte array
            regbytes = ByteBuffer.allocateDirect(tsh_count * 4);
            for (int i = 0; i < tsh_count * 4; i++) {
                regbytes.put(raf.readByte());
            }
        }

    }

    public class ThreadFPRegState extends ThreadState {
        public final int fsh_flavor;
        public final int fsh_count;
        ByteBuffer regbytes; // way too complex for individual declarations; do it all via native code

        ThreadFPRegState() throws IOException {
            fsh_flavor = readInt();
            fsh_count = readInt();
            regbytes = ByteBuffer.allocateDirect(fsh_count * 4);
            for (int i = 0; i < fsh_count * 4; i++) {
                regbytes.put(raf.readByte());
            }
        }

    }

    public class ThreadExceptionState extends ThreadState {
        public final int esh_flavor;
        public final int esh_count;
        public final int trapno;
        public final int err;
        public final long faultvaddr;

        ThreadExceptionState() throws IOException {
            super();
            esh_flavor = readInt();
            esh_count = readInt();
            trapno = readInt();
            err = readInt();
            faultvaddr = readLong();
        }

    }

    public class SymTabLoadCommand extends LoadCommand {
        public final int symoff;
        public final int nsyms;
        public final int stroff;
        public final int strsize;
        /**
         * Lazily created string table.
         */
        private byte[] stringTable;
        /**
         * Lazily created symbol table.
         */
        private NList64[] symbolTable;

        SymTabLoadCommand() throws IOException {
            super();
            symoff = readInt();
            nsyms = readInt();
            stroff = readInt();
            strsize = readInt();
        }

        public NList64[] getSymbolTable() throws IOException {
            if (symbolTable != null) {
                return symbolTable;
            }
            stringTable = new byte[strsize];
            raf.seek(stroff);
            for (int i = 0; i < strsize; i++) {
                stringTable[i] = raf.readByte();
            }
            symbolTable = new NList64[nsyms];
            raf.seek(symoff);
            for (int i = 0; i < nsyms; i++) {
                symbolTable[i] = new NList64();
            }
            return symbolTable;
        }

        public String getSymbolName(NList64 nlist64) {
            String symbol = "";
            if (nlist64.strx != 0) {
                byte sb = stringTable[nlist64.strx];
                int sl = 0;
                while (sb != 0) {
                    sb = stringTable[nlist64.strx + sl];
                    sl++;
                }
                // remove leading/trailing underscores which bracket all symbols
                symbol = new String(stringTable, nlist64.strx + 1, sl - 2);
            }
            return symbol;
        }

    }

    public class NList64 {
        public static final int STAB = 0xe0;
        public static final int PEXT = 0x10;
        public static final int TYPE = 0xe;
        public static final int EXT = 0x1;

        public static final int UNDF = 0x0;
        public static final int ABS  = 0x2;
        public static final int SECT = 0xe;
        public static final int PBUD = 0xc;
        public static final int INDR = 0xa;

        public final int strx;
        public final byte type;
        public final byte sect;
        public final short desc;
        public final long value;

        NList64() throws IOException {
            strx = readInt();
            type = raf.readByte();
            sect = raf.readByte();
            desc = readShort();
            value = readLong();
        }
    }

    /**
     * Locates a given section within a given array of load commands.
     * Sections are numbered from 1 as they occur within SEGMENT_64 commands.
     * @param loadCommands
     * @param sectToFind
     */
    public static Section64 getSection(LoadCommand[] loadCommands, int sectToFind) {
        int sect = 1;
        for (int i = 0; i < loadCommands.length; i++) {
            if (loadCommands[i].cmd == LoadCommand.LC_SEGMENT_64) {
                Segment64LoadCommand slc = (Segment64LoadCommand) loadCommands[i];
                if (sectToFind < sect + slc.nsects) {
                    return slc.sections[sectToFind - sect];
                }
                sect += slc.nsects;
            }
        }
        return null;
    }

    public short readShort() throws IOException {
        final int b1 = raf.read();
        final int b2 = raf.read();
        return (short) (((b2 << 8) | b1) & 0xFFFF);
    }

    public int readInt() throws IOException {
        final int b1 = raf.read();
        final int b2 = raf.read();
        final int b3 = raf.read();
        final int b4 = raf.read();
        return (b4 << 24) | (b3 << 16) | (b2 << 8) | b1;
    }

    public long readLong() throws IOException {
        final long lw = readInt();
        final long hw = readInt();
        return hw << 32 | (lw & 0xFFFFFFFFL);
    }

}
