package com.blogspot.developersu.ns_usbloader.pfs;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;

/**
 * Used in GoldLeaf USB protocol
 * */
public class PFSProvider {
    private static final byte[] PFS0 = new byte[]{(byte)0x50, (byte)0x46, (byte)0x53, (byte)0x30};  // PFS0, and what did you think?

    private BufferedInputStream bufferedInStream;
    private String nspFileName;
    private NCAFile[] ncaFiles;
    private long bodySize;
    private int ticketID = -1;

    public PFSProvider(InputStream inputStream, String nspFileName){
        if (inputStream == null || nspFileName == null)
            return;
        this.bufferedInStream = new BufferedInputStream(inputStream);      // TODO: refactor?
        this.nspFileName = nspFileName;
    }
    
    public boolean init() {
        if (nspFileName == null || bufferedInStream == null)
            return false;

        int filesCount;
        int stringTableSize;

        try {
            byte[] fileStartingBytes = new byte[12];
            // Read PFS0, files count, stringTableSize, padding (4 zero bytes)
            if (bufferedInStream.read(fileStartingBytes) != 12){
                bufferedInStream.close();
                return false;
            }
            // Check PFS0
            if (! Arrays.equals(PFS0, Arrays.copyOfRange(fileStartingBytes, 0, 4))){
                bufferedInStream.close();
                return false;
            }
            // Get files count
            filesCount = ByteBuffer.wrap(Arrays.copyOfRange(fileStartingBytes, 4, 8)).order(ByteOrder.LITTLE_ENDIAN).getInt();
            if (filesCount <= 0 ) {
                bufferedInStream.close();
                return false;
            }
            // Get stringTableSize
            stringTableSize = ByteBuffer.wrap(Arrays.copyOfRange(fileStartingBytes, 8, 12)).order(ByteOrder.LITTLE_ENDIAN).getInt();
            if (stringTableSize <= 0 ){
                bufferedInStream.close();
                return false;
            }
            //*********************************************************************************************
            // Create NCA set
            this.ncaFiles = new NCAFile[filesCount];
            // Collect files from NSP
            byte[] ncaInfoArr = new byte[24];   // should be unsigned long, but.. java.. u know my pain man

            HashMap<Integer, Long> ncaNameOffsets = new LinkedHashMap<>();

            long nca_offset;
            long nca_size;
            long nca_name_offset;

            for (int i=0; i < filesCount; i++){
                if (bufferedInStream.read(ncaInfoArr) != 24) {
                    bufferedInStream.close();
                    return false;
                }

                nca_offset = ByteBuffer.wrap(Arrays.copyOfRange(ncaInfoArr, 4, 12)).order(ByteOrder.LITTLE_ENDIAN).getLong();
                nca_size = ByteBuffer.wrap(Arrays.copyOfRange(ncaInfoArr, 12, 20)).order(ByteOrder.LITTLE_ENDIAN).getLong();
                nca_name_offset = ByteBuffer.wrap(Arrays.copyOfRange(ncaInfoArr, 20, 24)).order(ByteOrder.LITTLE_ENDIAN).getInt(); // yes, cast from int to long.

                NCAFile ncaFile = new NCAFile();
                ncaFile.setNcaOffset(nca_offset);
                ncaFile.setNcaSize(nca_size);
                this.ncaFiles[i] = ncaFile;

                ncaNameOffsets.put(i, nca_name_offset);
            }
            // Final offset
            byte[] bufForInt = new byte[4];
            if ((bufferedInStream.read(bufForInt) != 4))
                return false;

            // Calculate position including stringTableSize for body size offset
            //bodySize = bufferedInStream.getFilePointer()+stringTableSize;
            bodySize = filesCount*24+16+stringTableSize;
            //*********************************************************************************************
            bufferedInStream.mark(stringTableSize);
            // Collect file names from NCAs
            List<Byte> ncaFN;                 // Temporary
            byte[] b = new byte[1];                 // Temporary
            for (int i=0; i < filesCount; i++){
                ncaFN = new ArrayList<>();
                if (bufferedInStream.skip(ncaNameOffsets.get(i)) != ncaNameOffsets.get(i)) // Files cont * 24(bit for each meta-data) + 4 bytes goes after all of them  + 12 bit what were in the beginning
                    return false;
                while ((bufferedInStream.read(b)) != -1){
                    if (b[0] == 0x00)
                        break;
                    else
                        ncaFN.add(b[0]);
                }
                // TODO: CHANGE TO ncaFN.toArray();

                byte[] exchangeTempArray = new byte[ncaFN.size()];
                for (int j=0; j < ncaFN.size(); j++)
                    exchangeTempArray[j] = ncaFN.get(j);
                // Find and store ticket (.tik)
                if (new String(exchangeTempArray, "UTF-8").toLowerCase().endsWith(".tik"))
                    this.ticketID = i;
                this.ncaFiles[i].setNcaFileName(Arrays.copyOf(exchangeTempArray, exchangeTempArray.length));

                bufferedInStream.reset();
            }
            bufferedInStream.close();
        }
        catch (IOException ioe){
            return false;
        }
        return true;
    }
    /**
     * Return file name as byte array
     * */
    public byte[] getBytesNspFileName(){
        return nspFileName.getBytes();
    }
    /**
     * Return file name as String
     * */
    /*  Legacy code; leave for now
    public String getStringNspFileName(){
        return nspFileName;
    }
    */
    /**
     * Return file name length as byte array
     * */
    public byte[] getBytesNspFileNameLength(){
        return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(getBytesNspFileName().length).array();
    }
    /**
     * Return NCA count inside of file as byte array
     * */
    public byte[] getBytesCountOfNca(){
        return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ncaFiles.length).array();
    }
    /**
     * Return NCA count inside of file as int
     * */
    public int getIntCountOfNca(){
        return ncaFiles.length;
    }
    /**
     * Return requested-by-number NCA file inside of file
     * */
    public NCAFile getNca(int ncaNumber){
        return ncaFiles[ncaNumber];
    }
    /**
     * Return bodySize
     * */
    public long getBodySize(){
        return bodySize;
    }
    /**
     * Return special NCA file: ticket
     * (sugar)
     * */
    public int getNcaTicketID(){
        return ticketID;
    }
}
