/**
 * Copyright 2019 Adubbz
 * Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
package adubbz.nx.analyzer;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.compress.utils.Lists;
import org.python.google.common.collect.HashBiMap;
import org.python.google.common.collect.Sets;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;

import adubbz.nx.analyzer.ipc.IPCEmulator;
import adubbz.nx.analyzer.ipc.IPCTrace;
import adubbz.nx.common.ElfCompatibilityProvider;
import adubbz.nx.common.NXRelocation;
import adubbz.nx.loader.SwitchLoader;
import generic.stl.Pair;
import ghidra.app.services.AbstractAnalyzer;
import ghidra.app.services.AnalyzerType;
import ghidra.app.util.bin.format.elf.ElfSectionHeaderConstants;
import ghidra.app.util.demangler.DemangledObject;
import ghidra.app.util.demangler.DemanglerUtil;
import ghidra.app.util.importer.MessageLog;
import ghidra.framework.options.Options;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressOutOfBoundsException;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.data.DataTypeConflictException;
import ghidra.program.model.data.PointerDataType;
import ghidra.program.model.listing.CodeUnit;
import ghidra.program.model.listing.Data;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.Memory;
import ghidra.program.model.mem.MemoryAccessException;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.model.symbol.SourceType;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolTable;
import ghidra.program.model.util.CodeUnitInsertionException;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.InvalidInputException;
import ghidra.util.task.TaskMonitor;

public class IPCAnalyzer extends AbstractAnalyzer 
{
    public IPCAnalyzer() 
    {
        super("(Switch) IPC Analyzer", "Locates and labels IPC vtables, s_Tables and implementation functions.", AnalyzerType.INSTRUCTION_ANALYZER);
    
        this.setSupportsOneTimeAnalysis();
    }

    @Override
    public boolean getDefaultEnablement(Program program) 
    {
        return false;
    }

    @Override
    public boolean canAnalyze(Program program) 
    {
        return program.getExecutableFormat().equals(SwitchLoader.SWITCH_NAME);
    }

    @Override
    public void registerOptions(Options options, Program program) 
    {
        // TODO: Symbol options
    }

    @Override
    public boolean added(Program program, AddressSetView set, TaskMonitor monitor, MessageLog log) throws CancelledException 
    {
        Memory memory = program.getMemory();
        MemoryBlock text = memory.getBlock(".text");
        MemoryBlock rodata = memory.getBlock(".rodata");
        MemoryBlock data = memory.getBlock(".data");
        ElfCompatibilityProvider elfCompatProvider = new ElfCompatibilityProvider(program, false);
        
        Msg.info(this, "Beginning IPC analysis...");
        
        if (text == null || rodata == null || data == null)
            return true;
        
        try
        {
            List<Address> vtAddrs = this.locateIpcVtables(program, elfCompatProvider);
            List<IPCVTableEntry> vtEntries = this.createVTableEntries(program, elfCompatProvider, vtAddrs);
            HashBiMap<Address, Address> sTableProcessFuncMap = this.locateSTables(program, elfCompatProvider);
            Multimap<Address, IPCTrace> processFuncTraces = this.emulateProcessFunctions(program, monitor, sTableProcessFuncMap.values());
            HashBiMap<Address, IPCVTableEntry> procFuncVtMap = this.matchVtables(vtEntries, sTableProcessFuncMap.values(), processFuncTraces);
            this.markupIpc(program, monitor, vtEntries, sTableProcessFuncMap, processFuncTraces, procFuncVtMap);
        }
        catch (Exception e)
        {
            Msg.error(this, "Failed to analyze binary IPC.", e);
            return false;
        }
        
        return true;
    }
    
    private List<Address> locateIpcVtables(Program program, ElfCompatibilityProvider elfProvider) throws MemoryAccessException, AddressOutOfBoundsException, IOException
    {
        List<Address> out = Lists.newArrayList();
        Address baseAddr = program.getImageBase();
        AddressSpace aSpace = program.getAddressFactory().getDefaultAddressSpace();
        Memory mem = program.getMemory();
        SymbolTable symbolTable = program.getSymbolTable();
        
        Map<String, Address> knownVTabAddrs = new HashMap<>();
        Map<Address, Address> gotDataSyms = this.getGotDataSyms(program, elfProvider);
        
        if (gotDataSyms.isEmpty())
        {
            Msg.warn(this, "Failed to locate vtables - No got data symbols found!");
            return out;
        }
        
        Msg.info(this, "Locating IPC vtables...");
        
        // NOTE: We can't get the .<bla> block and check if it contains an address, as there may be multiple
        // blocks with the same name, which Ghidra doesn't account for.
        
        // Locate some initial vtables based on RTTI
        for (Address vtAddr : gotDataSyms.values()) 
        {
            MemoryBlock vtBlock = mem.getBlock(vtAddr);
            
            // vtables are only found in the data block
            if (vtBlock == null || !vtBlock.getName().equals(".data"))
                continue;
            
            try
            {
                Address rttiAddr = aSpace.getAddress(mem.getLong(vtAddr.add(8)));
                MemoryBlock rttiBlock = mem.getBlock(rttiAddr);
                
                // RTTI is only found in the data block
                if (rttiBlock == null || !rttiBlock.getName().equals(".data"))
                    continue;

                Address thisAddr = aSpace.getAddress(mem.getLong(rttiAddr.add(0x8)));
                MemoryBlock thisBlock = mem.getBlock(thisAddr);
                
                if (thisBlock == null || thisBlock.getName().equals(".rodata"))
                    continue;

                String symbol = elfProvider.getReader().readTerminatedString(thisAddr.getOffset(), '\0');

                if (symbol.isEmpty() || symbol.length() > 512)
                    continue;

                if (symbol.contains("UnmanagedServiceObject") || symbol.equals("N2nn2sf4cmif6server23CmifServerDomainManager6DomainE"))
                {
                    knownVTabAddrs.put(symbol, vtAddr);
                }
            }
            catch (MemoryAccessException e) // Skip entries with out of bounds offsets
            {
                continue;
            }
        }
        
        if (knownVTabAddrs.isEmpty())
        {
            Msg.warn(this, "Failed to locate vtables - No known addresses found!");
            return out;
        }
            
        // All IServiceObjects share a common non-overridable virtual function at vt + 0x20
        // and thus that value can be used to distinguish a virtual table vs a non-virtual table.
        // Here we locate the address of that function.
        long knownAddress = 0;
        
        for (Address addr : knownVTabAddrs.values())
        {
            long curKnownAddr = mem.getLong(addr.add(0x20));
            
            if (knownAddress == 0)
            {
                knownAddress = curKnownAddr; 
            }
            else if (knownAddress != curKnownAddr) 
                return out;
        }
        
        Msg.info(this, String.format("Known service address: 0x%x", knownAddress));
        
        // Use the known function to find all IPC vtables
        for (Address vtAddr : gotDataSyms.values()) 
        {
            MemoryBlock vtBlock = mem.getBlock(vtAddr);
            
            try
            {
                if (vtBlock != null && vtBlock.getName().equals(".data"))
                {
                    if (knownAddress == mem.getLong(vtAddr.add(0x20)))
                    {
                        out.add(vtAddr);
                    }
                }
            }
            catch (MemoryAccessException e) // Skip entries with out of bounds offsets
            {
                continue;
            }
        }
        
        return out;
    }
    
    protected List<IPCVTableEntry> createVTableEntries(Program program, ElfCompatibilityProvider elfProvider, List<Address> vtAddrs) throws MemoryAccessException, AddressOutOfBoundsException, IOException
    {
        List<IPCVTableEntry> out = Lists.newArrayList();
        Memory mem = program.getMemory();
        AddressSpace aSpace = program.getAddressFactory().getDefaultAddressSpace();
        
        for (Address vtAddr : vtAddrs)
        {
            long vtOff = vtAddr.getOffset();
            long rttiBase = mem.getLong(vtAddr.add(0x8));
            String name = String.format("SRV_%X::vtable", vtOff);
            
            // Attempt to find the name if the vtable has RTTI
            if (rttiBase != 0)
            {
                Address rttiBaseAddr = aSpace.getAddress(rttiBase);
                MemoryBlock rttiBaseBlock = mem.getBlock(rttiBaseAddr);
                
                // RTTI must be within the data block
                if (rttiBaseBlock != null && rttiBaseBlock.getName().equals(".data"))
                {
                    Address thisAddr = aSpace.getAddress(mem.getLong(rttiBaseAddr.add(0x8)));
                    MemoryBlock thisBlock = mem.getBlock(thisAddr);
                    
                    if (thisBlock != null && thisBlock.getName().equals(".rodata"))
                    {
                        String symbol = elfProvider.getReader().readTerminatedString(thisAddr.getOffset(), '\0');
                        
                        if (!symbol.isEmpty() && symbol.length() <= 512)
                        {
                            if (!symbol.startsWith("_Z"))
                                symbol = "_ZTV" + symbol;
                            
                            name = demangleIpcSymbol(symbol);
                        }
                    }
                }
            }
            
            Map<Address, Address> gotDataSyms = this.getGotDataSyms(program, elfProvider);
            List<Address> implAddrs = new ArrayList<>();
            long funcVtOff = 0x30;
            long funcOff = 0;
            
            // Find all ipc impl functions in the vtable
            while ((funcOff = mem.getLong(vtAddr.add(funcVtOff))) != 0)
            {
                Address funcAddr = aSpace.getAddress(funcOff);
                MemoryBlock funcAddrBlock = mem.getBlock(funcAddr);
                
                if (funcAddrBlock != null && funcAddrBlock.getName().equals(".text"))
                {
                    implAddrs.add(funcAddr);
                    funcVtOff += 0x8;
                }
                else break;
            
                if (gotDataSyms.values().contains(vtAddr.add(funcVtOff)))
                {
                    break;
                }
            }
            
            Set<Address> uniqueAddrs = new HashSet<Address>(implAddrs);
            
            // There must be either 1 unique function without repeats, or more than one unique function with repeats allowed
            if (uniqueAddrs.size() <= 1 && implAddrs.size() != 1)
            {
                Msg.warn(this, String.format("Insufficient unique addresses for vtable at 0x%X", vtAddr.getOffset()));
                
                for (Address addr : uniqueAddrs)
                {
                    Msg.info(this, String.format("    Found: 0x%X", addr.getOffset()));
                }
                
                implAddrs.clear();
            }
            
            // Some IPC symbols are very long and Ghidra crops them off far too early by default.
            // Let's shorten these.
            String shortName = shortenIpcSymbol(name);
            
            var entry = new IPCVTableEntry(name, shortName, vtAddr, implAddrs);
            Msg.info(this, String.format("VTable Entry: %s @ 0x%X", entry.abvName, entry.addr.getOffset()));
            out.add(entry);
        }
        
        return out;
    }
    
    protected HashBiMap<Address, Address> locateSTables(Program program, ElfCompatibilityProvider elfProvider)
    {
        HashBiMap<Address, Address> out = HashBiMap.create();
        List<Pair<Long, Long>> candidates = new ArrayList<>();
        AddressSpace aSpace = program.getAddressFactory().getDefaultAddressSpace();
        Address baseAddr = program.getImageBase();
        Memory mem = program.getMemory();
        
        for (NXRelocation reloc : elfProvider.getRelocations()) 
        {
            if (reloc.addend > 0)
                candidates.add(new Pair(baseAddr.getOffset() + reloc.addend, baseAddr.getOffset() + reloc.offset));
        }
        
        candidates.sort((a, b) -> a.first.compareTo(b.first));
        
        
        // 5.x: match on the "SFCI" constant used in the template of s_Table
        //   MOV  W?, #0x4653
        //   MOVK W?, #0x4943, LSL#16
        long movMask  = 0x5288CAL;
        long movkMask = 0x72A928L;
        
        MemoryBlock text = mem.getBlock(".text"); // Text is one of the few blocks that isn't split
        
        try
        {
            for (long off = text.getStart().getOffset(); off < text.getEnd().getOffset(); off += 0x4)
            {
                long val1 = (elfProvider.getReader().readUnsignedInt(off) & 0xFFFFFF00L) >> 8;
                long val2 = (elfProvider.getReader().readUnsignedInt(off + 0x4) & 0xFFFFFF00L) >> 8;
                
                // Match on a sequence of MOV, MOVK
                if (val1 == movMask && val2 == movkMask)
                {
                    long processFuncOffset = 0;
                    long sTableOffset = 0;
                    
                    // Find the candidate after our offset, then pick the one before that
                    for (Pair<Long, Long> candidate : candidates)
                    {
                        if (candidate.first > off)
                            break;
                        
                        processFuncOffset = candidate.first;
                        sTableOffset = candidate.second;
                    }
                    
                    long pRetOff;
                    
                    // Make sure our SFCI offset is within the process function by matching on the
                    // RET instruction
                    for (pRetOff = processFuncOffset; pRetOff < text.getEnd().getOffset(); pRetOff += 0x4)
                    {
                        long rval = elfProvider.getReader().readUnsignedInt(pRetOff);
                        
                        // RET
                        if (rval == 0xD65F03C0L)
                            break;
                    }
                    
                    if (pRetOff > off)
                    {
                        Address stAddr = aSpace.getAddress(sTableOffset);
                        Address pFuncAddr = aSpace.getAddress(processFuncOffset);
                        out.put(stAddr, pFuncAddr);
                    }
                }
            }
        }
        catch (IOException e)
        {
            Msg.error(this, "Failed to locate s_Tables", e);
        }
        
        return out;
    }
    
    protected Multimap<Address, IPCTrace> emulateProcessFunctions(Program program, TaskMonitor monitor, Set<Address> procFuncAddrs)
    {
        Multimap<Address, IPCTrace> out = HashMultimap.create();
        IPCEmulator ipcEmu = new IPCEmulator(program);
        Set<Integer> cmdsToTry = Sets.newHashSet();
        
        // Bruteforce 0-1000
        //for (int i = 0; i <= 1000; i++)
            //cmdsToTry.add(i);
        
        // The rest we add ourselves. From SwIPC. Duplicates are avoided by using a set
        int[] presets = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 4201, 106, 107, 108, 4205, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 20501, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 2413, 8216, 150, 151, 2201, 2202, 2203, 2204, 2205, 2207, 10400, 2209, 8219, 8220, 8221, 30900, 30901, 30902, 8223, 90300, 190, 8224, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 220, 20701, 222, 223, 230, 231, 250, 251, 252, 2301, 2302, 255, 256, 10500, 261, 2312, 280, 290, 291, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 2101, 20800, 20801, 322, 323, 2102, 8250, 350, 2400, 2401, 2402, 2403, 2404, 2405, 10600, 10601, 2411, 2412, 2450, 2414, 8253, 10610, 2451, 2421, 2422, 2424, 8255, 2431, 8254, 2433, 2434, 406, 8257, 400, 401, 402, 403, 404, 405, 10300, 407, 408, 409, 410, 411, 2460, 20900, 8252, 412, 2501, 10700, 10701, 10702, 8200, 1106, 1107, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, 512, 513, 520, 521, 90200, 8201, 90201, 540, 30810, 542, 543, 544, 545, 546, 30811, 30812, 8202, 8203, 8291, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 8295, 620, 8204, 8296, 630, 105, 640, 4203, 8225, 2050, 109, 30830, 2052, 8256, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 8207, 20600, 8208, 49900, 751, 11000, 127, 8209, 800, 801, 802, 803, 804, 805, 806, 821, 822, 823, 824, 8211, 850, 851, 852, 7000, 2055, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 3000, 3001, 3002, 160, 8012, 8217, 8013, 320, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1020, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041, 1042, 1043, 1044, 1045, 1046, 1047, 1061, 1062, 1063, 21000, 1100, 1101, 1102, 2053, 5202, 5203, 8218, 3200, 3201, 3202, 3203, 3204, 3205, 3206, 3207, 3208, 3209, 3210, 3211, 3214, 3215, 3216, 3217, 40100, 40101, 541, 1200, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 8292, 547, 20500, 8293, 2054, 2601, 8294, 40200, 40201, 1300, 1301, 1302, 1303, 1304, 8227, 20700, 221, 8228, 8297, 8229, 4206, 1400, 1401, 1402, 1403, 1404, 1405, 1406, 1411, 1421, 1422, 1423, 1424, 30100, 30101, 30102, 1431, 1432, 30110, 30120, 30121, 1451, 1452, 1453, 1454, 1455, 1456, 1457, 1458, 1471, 1472, 1473, 1474, 1500, 1501, 1502, 1503, 1504, 1505, 2300, 30200, 30201, 30202, 30203, 30204, 30205, 30210, 30211, 30212, 30213, 30214, 30215, 30216, 30217, 260, 1600, 1601, 1602, 1603, 60001, 60002, 30300, 2051, 20100, 20101, 20102, 20103, 20104, 20110, 1700, 1701, 1702, 1703, 8222, 30400, 30401, 30402, 631, 20200, 20201, 1800, 1801, 1802, 1803, 2008, 10011, 30500, 7992, 7993, 7994, 7995, 7996, 7997, 7998, 7999, 8000, 8001, 8002, 8011, 20300, 20301, 8021, 1900, 1901, 1902, 6000, 6001, 6002, 10100, 10101, 10102, 10110, 30820, 321, 1941, 1951, 1952, 1953, 8100, 20400, 20401, 8210, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 10200, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 10211, 2020, 2021, 30700, 2030, 2031, 8251, 90100, 90101, 90102 };
        
        for (int preset : presets)
            cmdsToTry.add(preset);
        
        Multimap<Address, IPCTrace> map = HashMultimap.create();
        
        int maxProgress = procFuncAddrs.size() * cmdsToTry.size();
        int progress = 0;
        
        monitor.setMessage("Emulating IPC process functions...");
        monitor.initialize(maxProgress);
        
        for (Address procFuncAddr : procFuncAddrs)
        {
            for (int cmd : cmdsToTry)
            {
                IPCTrace trace = ipcEmu.emulateCommand(procFuncAddr, cmd);
                
                if (trace.hasDescription())
                    map.put(procFuncAddr, trace);
            
                progress++;
                monitor.setProgress(progress);
            }
        }
        
        // Recreate the map as we can't sort the original
        for (Address procFuncAddr : map.keySet())
        {
            List<IPCTrace> traces = Lists.newArrayList(map.get(procFuncAddr).iterator());
            
            traces.sort((a, b) -> ((Long)a.cmdId).compareTo(b.cmdId));
            
            for (IPCTrace trace : traces)
            {
                out.put(procFuncAddr, trace);
            }
        }
        
        return out;
    }
    
    protected HashBiMap<Address, IPCVTableEntry> matchVtables(List<IPCVTableEntry> vtEntries, Set<Address> procFuncAddrs, Multimap<Address, IPCTrace> processFuncTraces)
    {
        // Map process func addrs to vtable addrs
        HashBiMap<Address, IPCVTableEntry> out = HashBiMap.create();
        List<IPCVTableEntry> possibilities = Lists.newArrayList(vtEntries.iterator());
        
        for (Address procFuncAddr : procFuncAddrs)
        {
            // We've already found this address. No need to do it again
            if (out.keySet().contains(procFuncAddr))
                continue;
            
            List<IPCVTableEntry> filteredPossibilities = possibilities.stream().filter(vtEntry -> vtEntry.ipcFuncs.size() == getProcFuncVTableSize(processFuncTraces, procFuncAddr)).collect(Collectors.toList());
            
            // See if there is a single entry that *exactly* matches the vtable size
            if (filteredPossibilities.size() == 1)
            {
                IPCVTableEntry vtEntry = filteredPossibilities.get(0);
                out.put(procFuncAddr, vtEntry);
                possibilities.remove(vtEntry);
                continue;
            }
            
            filteredPossibilities = possibilities.stream().filter(vtEntry -> vtEntry.ipcFuncs.size() >= getProcFuncVTableSize(processFuncTraces, procFuncAddr)).collect(Collectors.toList());

            // See if there is a single entry that is equal to or greater than the vtable size
            if (filteredPossibilities.size() == 1)
            {
                IPCVTableEntry vtEntry = filteredPossibilities.get(0);
                out.put(procFuncAddr, vtEntry);
                possibilities.remove(vtEntry);
                continue;
            }
            
            // Iterate over all the possible vtables with a size greater than our current process function
            for (IPCVTableEntry filteredPossibility : filteredPossibilities)
            {
                List<Address> unlocatedProcFuncAddrs = procFuncAddrs.stream().filter(pFAddr -> !out.keySet().contains(pFAddr)).collect(Collectors.toList());
                
                // See if there is only a single trace set of size <= this vtable
                // For example, if the process func vtable size is found by emulation to be 0x100, and we have previously found vtables of the following sizes, which have yet to be located:
                // 0x10, 0x20, 0x60, 0x110, 0x230
                // We will run this loop for both 0x110 and 0x230. 
                // In the case of 0x110, we will then filter for sizes <= 0x110. These are 0x10, 0x20, 0x60 and 0x110
                // As there are four of these, the check will fail.
                if (unlocatedProcFuncAddrs.stream().filter(unlocatedProcFuncAddr -> getProcFuncVTableSize(processFuncTraces, unlocatedProcFuncAddr) <= filteredPossibility.ipcFuncs.size()).collect(Collectors.toList()).size() == 1)
                {
                    out.put(procFuncAddr, filteredPossibility);
                    possibilities.remove(filteredPossibility);
                    break;
                }
            }
        }
        
        List<Address> unlocatedProcFuncAddrs = procFuncAddrs.stream().filter(pFAddr -> !out.keySet().contains(pFAddr)).collect(Collectors.toList());
        
        for (Address addr : unlocatedProcFuncAddrs)
        {
            Msg.info(this, String.format("Unmatched process func at 0x%X. Calculated VTable Size: 0x%X", addr.getOffset(), getProcFuncVTableSize(processFuncTraces, addr)));
        }
        
        for (IPCVTableEntry entry : possibilities)
        {
            Msg.info(this, String.format("Unmatched IPC VTable entry at 0x%X. VTable Size: 0x%X", entry.addr.getOffset(), entry.ipcFuncs.size()));
        }
        
        return out;
    }
    
    protected void markupIpc(Program program, TaskMonitor monitor, List<IPCVTableEntry> vtEntries, HashBiMap<Address, Address> sTableProcessFuncMap, Multimap<Address, IPCTrace> processFuncTraces, HashBiMap<Address, IPCVTableEntry> procFuncVtMap)
    {
        AddressSpace aSpace = program.getAddressFactory().getDefaultAddressSpace();
        
        try
        {
            // Analyze and label any IPC info found
            for (IPCVTableEntry entry : vtEntries)
            {
                List<IPCTrace> ipcTraces = Lists.newArrayList();
                Address processFuncAddr = procFuncVtMap.inverse().get(entry);
                
                if (processFuncAddr != null)
                {
                    Address sTableAddr = sTableProcessFuncMap.inverse().get(processFuncAddr);
                    String ipcComment = ""                +
                            "IPC INFORMATION\n"           +
                            "s_Table Address:       0x%X";
                    
                    if (sTableAddr != null)
                    {
                        ipcComment = String.format(ipcComment, sTableAddr.getOffset());
                        program.getListing().setComment(entry.addr, CodeUnit.PLATE_COMMENT, ipcComment);
                    }
                    
                    ipcTraces = Lists.newArrayList(processFuncTraces.get(processFuncAddr).iterator());
                }
                    
                String entryNameNoSuffix = entry.abvName.replace("::vtable", "");
                
                // Set the vtable name
                if (!this.hasImportedSymbol(program, entry.addr))
                {
                    // For shortened names, leave a comment so the user knows what the original name is
                    if (entry.fullName != entry.abvName)
                        program.getListing().setComment(entry.addr, CodeUnit.REPEATABLE_COMMENT, entry.fullName);
                    
                    Msg.info(this, String.format("Creating label for %s @ 0x%X", entry.abvName, entry.addr.getOffset()));
                    program.getSymbolTable().createLabel(entry.addr, entry.abvName, null, SourceType.IMPORTED);
                }
                
                // Label the four functions that exist for all ipc vtables
                for (int i = 0; i < 4; i++)
                {
                    Address vtAddr = entry.addr.add(0x10 + i * 0x8);
                    String name = "";
                    
                    // Set vtable func data types to pointers
                    this.createPointer(program, vtAddr);
                    
                    switch (i)
                    {
                        case 0:
                            name = "AddReference";
                            break;
                            
                        case 1:
                            name = "Release";
                            break;
                            
                        case 2:
                            name = "GetProxyInfo";
                            break;
                            
                        case 3: // Shared by everything
                            name = "nn::sf::IServiceObject::GetInterfaceTypeInfo";
                            break;
                    }
                             
                    if (i == 3) // For now, only label GetInterfaceTypeInfo. We need better heuristics for the others as they may be shared.
                    {
                        Address funcAddr = aSpace.getAddress(program.getMemory().getLong(vtAddr));
                        
                        if (!this.hasImportedSymbol(program, funcAddr))
                            program.getSymbolTable().createLabel(funcAddr, name, null, SourceType.IMPORTED);
                    }
                    else
                    {
                        program.getListing().setComment(vtAddr, CodeUnit.REPEATABLE_COMMENT, name);
                    }
                }
                
                for (int i = 0; i < entry.ipcFuncs.size(); i++)
                {
                    Address func = entry.ipcFuncs.get(i);
                    String name = null;
    
                    // Set vtable func data types to pointers
                    this.createPointer(program, entry.addr.add(0x30 + i * 0x8));
                }
                
                for (IPCTrace trace : ipcTraces)
                {
                    // Safety precaution. I *think* these should've been filtered out earlier though.
                    if (trace.vtOffset == -1 || !trace.hasDescription())
                        continue;
                    
                    Address vtOffsetAddr = entry.addr.add(0x10 + trace.vtOffset);
                    Address ipcCmdImplAddr = aSpace.getAddress(program.getMemory().getLong(vtOffsetAddr));
                    
                    if (!this.hasImportedSymbol(program, ipcCmdImplAddr))
                        program.getSymbolTable().createLabel(ipcCmdImplAddr, String.format("%s::Cmd%d", entryNameNoSuffix, trace.cmdId), null, SourceType.IMPORTED);
                    
                    String implComment = ""         +
                            "IPC INFORMATION\n"       +
                            "Bytes In:       0x%X\n"  +
                            "Bytes Out:      0x%X\n"  +
                            "Buffer Count:   0x%X\n"  +
                            "In Interfaces:  0x%X\n"  +
                            "Out Interfaces: 0x%X\n"  +
                            "In Handles:     0x%X\n"  +
                            "Out Handles:    0x%X";
                    
                    implComment = String.format(implComment, trace.bytesIn, trace.bytesOut, trace.bufferCount, trace.inInterfaces, trace.outInterfaces, trace.inHandles, trace.outHandles);
                    program.getListing().setComment(ipcCmdImplAddr, CodeUnit.PLATE_COMMENT, implComment);
                }
            }
            
            // Annotate s_Tables
            for (Address stAddr : sTableProcessFuncMap.keySet())
            {
                this.createPointer(program, stAddr);
                
                if (!this.hasImportedSymbol(program, stAddr))
                {
                    Address procFuncAddr = sTableProcessFuncMap.get(stAddr);
                    String sTableName = String.format("SRV_S_TAB_%X", stAddr.getOffset());
                    
                    if (procFuncAddr != null)
                    {
                        IPCVTableEntry entry = procFuncVtMap.get(procFuncAddr);
                        
                        if (entry != null)
                        {
                            String entryNameNoSuffix = entry.abvName.replace("::vtable", "");
                            sTableName = entryNameNoSuffix + "::s_Table";
                        }
                    }
                    
                    program.getSymbolTable().createLabel(stAddr, sTableName, null, SourceType.IMPORTED);
                }
            }
        }
        catch (InvalidInputException | AddressOutOfBoundsException | MemoryAccessException e)
        {
            Msg.error(this, "Failed to markup IPC", e);
        }
    }
    
    protected int getProcFuncVTableSize(Multimap<Address, IPCTrace> processFuncTraces, Address procFuncAddr)
    {
        if (!processFuncTraces.containsKey(procFuncAddr) || processFuncTraces.get(procFuncAddr).isEmpty())
            return 0;
        
        IPCTrace maxTrace = null;
        
        for (IPCTrace trace : processFuncTraces.get(procFuncAddr))
        {
            if (trace.vtOffset == -1)
                continue;
            
            if (maxTrace == null || trace.vtOffset > maxTrace.vtOffset)
                maxTrace = trace;
        }
        
        return (int)Math.max(processFuncTraces.get(procFuncAddr).size(), (maxTrace.vtOffset + 8 - 0x20) / 8);
    }
    
    private Map<Address, Address> gotDataSyms = null;
    
    /**
     * A map of relocated entries in the global offset table to their new values.
     */
    protected Map<Address, Address> getGotDataSyms(Program program, ElfCompatibilityProvider elfProvider)
    {
        if (gotDataSyms != null)
            return this.gotDataSyms;
        
        Address baseAddr = program.getImageBase();
        gotDataSyms = new HashMap<Address, Address>();
        
        for (NXRelocation reloc : elfProvider.getRelocations()) 
        {
            long off;
            
            if (reloc.sym != null && reloc.sym.getSectionHeaderIndex() != ElfSectionHeaderConstants.SHN_UNDEF && reloc.sym.getValue() == 0)
            {
                off = reloc.sym.getValue();
            }
            else if (reloc.addend != 0)
            {
                off = reloc.addend;
            }
            else continue;
            
            // Target -> Value
           this.gotDataSyms.put(baseAddr.add(reloc.offset), baseAddr.add(off));
        }
        
        return gotDataSyms;
    }
    
    public static String demangleIpcSymbol(String mangled)
    {
        // Needed by the demangler
        if (!mangled.startsWith("_Z"))
            mangled = "_Z" + mangled;
     
        String out = mangled;
        DemangledObject demangledObj = DemanglerUtil.demangle(mangled);
        
        // Where possible, replace the mangled symbol with a demangled one
        if (demangledObj != null)
        {
            StringBuilder builder = new StringBuilder(demangledObj.toString());
            int templateLevel = 0;
            
            //De-Ghidrify-template colons
            for (int i = 0; i < builder.length(); ++i) 
            {
                char ch = builder.charAt(i);
                
                if (ch == '<') 
                {
                    ++templateLevel;
                }
                else if (ch == '>' && templateLevel != 0) 
                {
                    --templateLevel;
                }

                if (templateLevel > 0 && ch == '-') 
                    builder.setCharAt(i, ':');
            }
            
            out = builder.toString();
        }            
        
        return out;
    }
    
    public static String shortenIpcSymbol(String longSym)
    {
        String out = longSym;
        String suffix = out.substring(out.lastIndexOf(':') + 1);
        
        if (out.startsWith("nn::sf::detail::ObjectImplFactoryWithStatelessAllocator<") || out.startsWith("nn::sf::detail::ObjectImplFactoryWithStatefulAllocator<"))
        {
            String abvNamePrefixOld = "nn::sf::detail::EmplacedImplHolder<";
            String abvNamePrefixNew = "_tO2N<";
            
            int abvNamePrefixOldIndex = out.indexOf(abvNamePrefixOld);
            int abvNamePrefixNewIndex = out.indexOf(abvNamePrefixNew);
            
            if (abvNamePrefixOldIndex != -1)
            {
                int abvNameStart = abvNamePrefixOldIndex + abvNamePrefixOld.length();
                out = out.substring(abvNameStart, out.indexOf(',', abvNameStart));
            }
            else if (abvNamePrefixNewIndex != -1)
            {
                int abvNameStart = abvNamePrefixNewIndex + abvNamePrefixNew.length();
                out = out.substring(abvNameStart, out.indexOf('>', abvNameStart));
            }
            
            out += "::" + suffix;
        }

        return out;
    }
    
    public boolean hasImportedSymbol(Program program, Address addr)
    {
        for (Symbol sym : program.getSymbolTable().getSymbols(addr))
        {
            if (sym.getSource() == SourceType.IMPORTED)
                return true;
        }
        
        return false;
    }
    
    protected int createPointer(Program program, Address address)
    {
        Data d = program.getListing().getDataAt(address);
        
        if (d == null) 
        {
            try 
            {
                d = program.getListing().createData(address, PointerDataType.dataType, 8);
            } 
            catch (CodeUnitInsertionException | DataTypeConflictException e) 
            {
                Msg.error(this, String.format("Failed to create pointer at 0x%X", address.getOffset()), e);
            }
        }
        
        return d.getLength();
    }
    
    public static class IPCVTableEntry
    {
        public final String fullName;
        public final String abvName;
        public final Address addr;
        public final ImmutableList<Address> ipcFuncs;
        
        private IPCVTableEntry(String fullName, String abvName, Address addr, List<Address> ipcFuncs)
        {
            this.fullName = fullName;
            this.abvName = abvName;
            this.addr = addr;
            this.ipcFuncs = ImmutableList.copyOf(ipcFuncs);
        }
    }
}