/*
 * Copyright (C) 2012 The Android Open Source Project 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at 
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */

package com.android.jobb;

import de.waldheinz.fs.BlockDevice;
import de.waldheinz.fs.FsDirectory;
import de.waldheinz.fs.FsDirectoryEntry;
import de.waldheinz.fs.FsFile;
import de.waldheinz.fs.fat.BootSector;
import de.waldheinz.fs.fat.Fat;
import de.waldheinz.fs.fat.FatFile;
import de.waldheinz.fs.fat.FatFileSystem;
import de.waldheinz.fs.fat.FatLfnDirectory;
import de.waldheinz.fs.fat.FatLfnDirectoryEntry;
import de.waldheinz.fs.fat.FatType;
import de.waldheinz.fs.fat.FatUtils;
import de.waldheinz.fs.fat.SuperFloppyFormatter;
import de.waldheinz.fs.util.FileDisk;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.Stack;

public class Main {

    private static final int BLOCK_SIZE = 512; // MUST BE 512

    public static void printArgs() {
        System.out.println("Jobb -- Create OBB files for use on Android");
        System.out.println();
        System.out.println(" -d <directory> Use <directory> as input/output for OBB files");
        System.out.println(" -k <key>       Use <key> as password to encrypt/decrypt OBB file");
        System.out.println(" -o <filename>  Write OBB file out to <filename>");
        System.out.println(" -v             Verbose mode");
        System.out.println(" -h             Help; this usage screen");
        System.out.println(" -pn <package>  Package name for OBB file");
        System.out.println(" -pv <version>  Package version for OBB file");
        System.out.println(" -ov            Set overlay flag");
        System.out.println(" -dump <file>   Parse and dump OBB file");        
        System.out.println(" -about         Notices about this tool");
        System.out.println();
        System.out.println("Example: Dump the contents of the encrypted OBB file to the directory");
        System.out.println("         jobb -dump myfile.obb -d ./mydirectory -k mypassword");
        System.out.println();
        System.out.println("Example: Create an encrypted OBB file by recursing the directory path");
        System.out.println("         jobb -pn package.name -pv 1 -d ./mydirectory -k mypassword");
    }

    static String sDirectory;
    static File sDirectoryFile;
    static boolean sHasOutputDirectory;
    static String sKey;
    static String sOutputFile;
    static boolean sVerboseMode;
    static String sPackageName;
    static int sPackageVersion = -1;
    static byte[] sSalt;
    static boolean sOverlay;
    static int sFlags;
    static String sInputFile;
    static byte[] sFishKey;

    private interface FileProcessor {
        void processFile(File f);

        void processDirectory(File f);

		/**
		 * @param dir
		 */
		void endDirectory(File dir);
    }

    static ByteBuffer sTempBuf = ByteBuffer.allocate(1024*1024);
    
    static public void dumpDirectory(FsDirectory dir, int tabStop, File curDirectory) throws IOException {
        Iterator<FsDirectoryEntry> i = dir.iterator();
        while (i.hasNext()) {
            final FsDirectoryEntry e = i.next();
            if (e.isDirectory()) {
                for (int idx = 0; idx < tabStop; idx++)
                    System.out.print(' ');
                if (e.getName().equals(".") || e.getName().equals(".."))
                    continue;
                for (int idx = 0; idx < tabStop; idx++)
                    System.out.print("  ");
                System.out.println("[" + e + "]");
                dumpDirectory(e.getDirectory(), tabStop + 1, new File(curDirectory, e.getName()));
            } else {
                for ( int idx = 0; idx < tabStop; idx++ ) System.out.print("  ");
                System.out.println( e );
                if ( sHasOutputDirectory ) {
                    if ( !curDirectory.exists() ) {
                        if ( false == curDirectory.mkdirs() ) {
                            throw new IOException("Unable to create directory: " + curDirectory);                            
                        }
                    }
                    File curFile = new File(curDirectory, e.getName());
                    if ( curFile.exists() ) {
                        throw new IOException("File exists: " + curFile); 
                    } else {
                        FsFile f = e.getFile();
                        FileOutputStream fos = null;
                        try {
                            fos = new FileOutputStream(curFile);
                            FileChannel outputChannel = fos.getChannel();
                            int capacity = sTempBuf.capacity();
                            long length = f.getLength();
                            for ( long pos = 0; pos < length; pos++ ) {
                                int readLength = (int)(length-pos > capacity ? capacity : length-pos);
                                sTempBuf.rewind();
                                sTempBuf.limit(readLength);
                                f.read(pos, sTempBuf);
                                sTempBuf.rewind();
                                while(sTempBuf.remaining() > 0)
                                    outputChannel.write(sTempBuf);
                                pos += readLength;
                            }
                        } finally {
                            if ( null != fos ) fos.close();
                        }
                    }
                }
            }
        }
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        boolean displayHelp = false;
        boolean isEncrypted = false;
        try {
            for (int i = 0; i < args.length; i++) {
                String curArg = args[i];
                if (curArg.equals("-d")) {
                    sDirectory = args[++i];
                    sDirectoryFile = new File(sDirectory);
                    sHasOutputDirectory = true;
                } else if (curArg.equals("-k")) {
                    isEncrypted = true;
                    sKey = args[++i];
                } else if (curArg.equals("-o")) {
                    sOutputFile = args[++i];
                } else if (curArg.equals("-h")) {
                    displayHelp = true;
                } else if (curArg.equals("-v")) {
                    sVerboseMode = true;
                } else if (curArg.equals("-pn")) {
                    sPackageName = args[++i];
                } else if (curArg.equals("-pv")) {
                    sPackageVersion = Integer.parseInt(args[++i]);
                } else if (curArg.equals("-ov")) {
                    sFlags |= ObbFile.OBB_OVERLAY;
                } else if (curArg.equals("-dump")) {
                    sInputFile = args[++i];
                } else if (curArg.equals("-salt")) {
                    // special case --- use same salt
                    String saltString = args[++i];
                    BigInteger bi = new BigInteger(saltString, 16);
                    sSalt = bi.toByteArray();
                    if ( sSalt.length != PBKDF.SALT_LEN ) {
                        displayHelp = true;
                    }
                } else if (curArg.equals("-about")) {
                    System.out.println("-------------------------------------------------------------------------------");
                    System.out.println("Portions of this code:");             
                    System.out.println("-------------------------------------------------------------------------------");
                    System.out.println("Copyright (c) 2000 The Legion Of The Bouncy Castle");
                    System.out.println("(http://www.bouncycastle.org)");
                    System.out.println();
                    System.out.println("Permission is hereby granted, free of charge, to any person obtaining");
                    System.out.println("a copy of this software and associated documentation files (the \"Software\"");
                    System.out.println("to deal in the Software without restriction, including without limitation");
                    System.out.println("the rights to use, copy, modify, merge, publish, distribute, sublicense");
                    System.out.println("and/or sell copies of the Software, and to permit persons to whom the Software");
                    System.out.println("is furnished to do so, subject to the following conditions:");
                    System.out.println();
                    System.out.println("The above copyright notice and this permission notice shall be included in all");
                    System.out.println("copies or substantial portions of the Software.");
                    System.out.println("-------------------------------------------------------------------------------");
                    System.out.println("Twofish is uncopyrighted and license-free, and was created and analyzed by:");                   
                    System.out.println("Bruce Schneier - John Kelsey - Doug Whiting");
                    System.out.println("David Wagner - Chris Hall - Niels Ferguson");
                    System.out.println("-------------------------------------------------------------------------------");
                    System.out.println("Cryptix General License");
                    System.out.println();
                    System.out.println("Copyright (c) 1995-2005 The Cryptix Foundation Limited.");
                    System.out.println("All rights reserved.");
                    System.out.println("");
                    System.out.println("Redistribution and use in source and binary forms, with or without");
                    System.out.println("modification, are permitted provided that the following conditions are");
                    System.out.println("met:");
                    System.out.println();
                    System.out.println(" 1. Redistributions of source code must retain the copyright notice,");
                    System.out.println("    this list of conditions and the following disclaimer.");
                    System.out.println(" 2. Redistributions in binary form must reproduce the above copyright");
                    System.out.println("    notice, this list of conditions and the following disclaimer in");
                    System.out.println("    the documentation and/or other materials provided with the");
                    System.out.println("    distribution.");
                    System.out.println("-------------------------------------------------------------------------------");
                    return;
                }
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            displayHelp = true;
        }
        if (null != sInputFile) {
            ObbFile obbFile = new ObbFile();
            obbFile.readFrom(sInputFile);
            System.out.print("Package Name: ");
            System.out.println(obbFile.mPackageName);
            System.out.print("Package Version: ");
            System.out.println(obbFile.mPackageVersion);            
            if (0 != (obbFile.mFlags & ObbFile.OBB_SALTED)) {
                System.out.print("SALT: ");
                BigInteger bi = new BigInteger(obbFile.mSalt);
                System.out.println(bi.toString(16));
                System.out.println();
                if ( null == sKey ) {
                    System.out.println("Encrypted file. Please add password.");
                    return;
                }
                try {
                    sFishKey = PBKDF.getKey(sKey,obbFile.mSalt);
                    bi = new BigInteger(sFishKey);
                    System.out.println(bi.toString(16));                    
                } catch (InvalidKeyException e) {
                    e.printStackTrace();
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                isEncrypted = true;
            } else {
                isEncrypted = false;
            }
            File obbInputFile = new File(sInputFile);

            BlockDevice fd;
            try {
                if ( isEncrypted ) {
                    EncryptedBlockFile ebf = new EncryptedBlockFile(sFishKey, obbInputFile, "r");
                    fd = new FileDisk(ebf, ebf.getEncryptedFileChannel(), true);
                } else {
                    fd = new FileDisk(obbInputFile, true);
                }
                final FatFileSystem fatFs = FatFileSystem.read(fd, true);
                final BootSector bs = fatFs.getBootSector();
                final FsDirectory rootDir = fatFs.getRoot();
                if (sVerboseMode) {
                    System.out.print("Filesystem Type: ");
                    FatType ft = bs.getFatType();
                    if (ft == FatType.FAT32) {
                        System.out.println("FAT32");
                    } else if (ft == FatType.FAT16) {
                        System.out.println("FAT16");
                    } else if (ft == FatType.FAT12) {
                        System.out.println("FAT12");
                    } else {
                        System.out.println("Unknown");
                    }
                    System.out.print("           OEM Name: ");
                    System.out.println(bs.getOemName());
                    System.out.print("   Bytes Per Sector: ");
                    System.out.println(bs.getBytesPerSector());
                    System.out.print("Sectors per cluster: ");
                    System.out.println(bs.getSectorsPerCluster());
                    System.out.print("   Reserved Sectors: ");
                    System.out.println(bs.getNrReservedSectors());
                    System.out.print("               Fats: ");
                    System.out.println(bs.getNrFats());
                    System.out.print("   Root Dir Entries: ");
                    System.out.println(bs.getRootDirEntryCount());
                    System.out.print("  Medium Descriptor: ");
                    System.out.println(bs.getMediumDescriptor());
                    System.out.print("            Sectors: ");
                    System.out.println(bs.getSectorCount());
                    System.out.print("    Sectors Per Fat: ");
                    System.out.println(bs.getSectorsPerFat());
                    System.out.print("              Heads: ");
                    System.out.println(bs.getNrHeads());
                    System.out.print("     Hidden Sectors: ");
                    System.out.println(bs.getNrHiddenSectors());
                    System.out.print("         Fat Offset: ");
                    System.out.println(FatUtils.getFatOffset(bs, 0));
                    System.out.println("          RootDir: " + rootDir);
                }
                dumpDirectory(rootDir, 0, sDirectoryFile);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            }
            return;
        }
        boolean printArgs;
        if (displayHelp) {
        	printArgs = true;
        } else if ( null == sDirectory ) {
        	printArgs = true;
        	System.out.println("A directory to be recursed through [-d directory] is required when creating an OBB filesystem.");
        } else if ( null == sOutputFile ) {
        	printArgs = true;
        	System.out.println("An output filename [-o outputfile] is required when creating an OBB filesystem.");        	
        } else if ( null == sPackageName ) {
        	printArgs = true;
        	System.out.println("A package name [-pn package] is required when creating an OBB filesystem.");        	        	
        } else if ( -1 == sPackageVersion ) {
        	printArgs = true;        	
        	System.out.println("A package version [-pv package] is required when creating an OBB filesystem.");        	        	
        } else {
        	printArgs = false;
        }
        if (printArgs) {
            printArgs();
        } else {
            if (null != sKey) {
                if ( null == sSalt ) {
                    sSalt = PBKDF.getRandomSalt();
                }
                try {
                    sFishKey = PBKDF.getKey(sKey, sSalt);
                } catch (Exception e) {
                    e.printStackTrace();
                    return;
                }
                sFlags |= ObbFile.OBB_SALTED;
                if (sVerboseMode) {
                    System.out.println("Crypto: ");
                    System.out.print("SALT: ");
                    for (byte b : sSalt) {
                        int out = b & 0xFF;
                        System.out.print(Integer.toHexString(out));
                        System.out.print(" ");
                    }
                    System.out.print("\t");
                    System.out.print("KEY: ");
                    for (byte b : sFishKey) {
                        int out = b & 0xFF;
                        System.out.print(Integer.toHexString(out));
                        System.out.print(" ");
                    }
                    System.out.println();
                }
            }
            if (sVerboseMode) {
                System.out.println("Scanning directory: " + sDirectory);
            }
            final File f = new File(sDirectory);
            
            long fileSize = getTotalFileSize(f, 0);
            fileSize = getTotalFileSize(f, BLOCK_SIZE*SuperFloppyFormatter.clusterSizeFromSize(fileSize, BLOCK_SIZE));
            if (sVerboseMode) {
                System.out.println("Total Files: " + fileSize);
            }
            long numSectors = fileSize / BLOCK_SIZE;
            long clusterSize = SuperFloppyFormatter.clusterSizeFromSize(fileSize, BLOCK_SIZE);
            long fatOverhead = 2*numSectors/clusterSize*4;
            fatOverhead += clusterSize*BLOCK_SIZE - fatOverhead % (clusterSize*BLOCK_SIZE);
            fatOverhead += 2*clusterSize; //start at second cluster
            if (sVerboseMode) {
                System.out.println("FAT Overhead: " + fatOverhead);
            }
            long clusterSizeInBytes = clusterSize * BLOCK_SIZE;
            long filesystemSize = (( numSectors ) * BLOCK_SIZE + fatOverhead + clusterSizeInBytes -1 ) / clusterSizeInBytes * clusterSizeInBytes;            
            if (sVerboseMode) {
                System.out.println("Filesystem Size: " + filesystemSize);
            }
            File fsFile = new File(sOutputFile);
            if (fsFile.exists())
                fsFile.delete();
            try {
                BlockDevice fd;
                if ( isEncrypted ) {
                    try {
                        EncryptedBlockFile ebf = new EncryptedBlockFile(sFishKey, fsFile, "rw");
                        ebf.setLength(filesystemSize);
                        fd = new FileDisk(ebf, ebf.getEncryptedFileChannel(), false);
                    } catch (InvalidKeyException e) {
                        e.printStackTrace();
                        return;
                    }
                } else {
                    fd = FileDisk.create(fsFile, filesystemSize);
                }
                // fat type set based on device size by SuperFloppyFormatter
                final FatFileSystem fs = SuperFloppyFormatter.get(fd).format();
                final String rootPath = f.getAbsolutePath();
                // add the files into the filesystem
                processAllFiles(f, new FileProcessor() {
                    Stack<FatLfnDirectory> mCurDir = new Stack<FatLfnDirectory>();

                    @Override
                    public void processDirectory(File curFile) {
                        String directory = curFile.getAbsolutePath().substring(rootPath.length());
                        if (sVerboseMode) {
                            System.out.println("Processing Directory: " + directory + " at cluster " + fs.getFat().getLastFreeCluster());
                        }
                        FatLfnDirectory curDir = fs.getRoot();
                        if (directory.length() > 0) {
                            File tempFile = new File(directory);
                            Stack<String> pathStack = new Stack<String>();
                            do {
                                pathStack.push(tempFile.getName());
                            } while (null != (tempFile = tempFile.getParentFile()));
                            while (!pathStack.empty()) {
                                String name = pathStack.pop();
                                if (0 == name.length())
                                    continue;
                                FatLfnDirectoryEntry entry = curDir.getEntry(name);
                                if (null != entry) {
                                    if (!entry.isDirectory()) {
                                        throw new RuntimeException(
                                                "File path not FAT compatible - naming conflict!");
                                    }
                                } else {
                                    try {
                                        if (sVerboseMode) {
                                            System.out.println("Adding Directory: " + name);
                                        }
                                        entry = curDir.addDirectory(name);
                                    } catch (IOException e) {
                                        e.printStackTrace();
                                        throw new RuntimeException("Error adding directory!");
                                    }
                                }
                                try {
                                    curDir = entry.getDirectory();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                    throw new RuntimeException("Error getting directory");
                                }
                            }
                        }
                        mCurDir.push(curDir);
                    }

                    @Override
                    public void processFile(File curFile) {
                        FatLfnDirectoryEntry entry;
                        FatLfnDirectory curDir = mCurDir.peek();
                        try {
                            if (sVerboseMode) {
                                System.out.println("Adding file: "
                                        + curFile.getAbsolutePath().substring(rootPath.length())
                                        + " with length " + curFile.length() + " at cluster " + fs.getFat().getLastFreeCluster());
                            }
                            entry = curDir.addFile(curFile.getName());
                        } catch (IOException e) {
                            e.printStackTrace();
                            throw new RuntimeException("Error adding file with name: "
                                    + curFile.getName());
                        }
                        ReadableByteChannel channel = null;
                        try {
                            FatFile f = entry.getFile();
                            channel = new FileInputStream(curFile).getChannel();
                            ByteBuffer buf = ByteBuffer.allocateDirect(1024 * 512);
                            int numRead = 0;
                            long offset = 0;
                            while (true) {
                                buf.clear();
                                numRead = channel.read(buf);
                                if (numRead < 0)
                                    break;
                                buf.rewind();
                                buf.limit(numRead);
                                f.write(offset, buf);
                                offset += numRead;
                            }
                            f.flush();
                        } catch (IOException e) {
                            e.printStackTrace();
                            throw new RuntimeException("Error getting/writing file with name: "
                                    + curFile.getName());
                        } finally {
                            if (null != channel)
                                try {
                                    channel.close();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                        }
                    }

					@Override
					public void endDirectory(File dir) {
						mCurDir.pop();
					}
					
                });
                fs.flush();
                fs.close();
                Fat fat = fs.getFat();
                ObbFile ob = new ObbFile();
                ob.setPackageName(sPackageName);
                ob.setPackageVersion(sPackageVersion);
                ob.setFlags(sFlags);
                if (null != sSalt) {
                    ob.setSalt(sSalt);
                }
                ob.writeTo(fsFile);                
                if (sVerboseMode) {
                    System.out.println("Success!");
                    System.out.println("" + fs.getTotalSpace() + " bytes total");
                    System.out.println("" + fs.getFreeSpace() + " bytes free");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static long getTotalFileSize(File dir, final int clusterSize) {
        final long[] mSize = new long[3];
        final boolean calculateSlop = clusterSize > 0;
        processAllFiles(dir, new FileProcessor() {
            Stack<int[]> mDirLen = new Stack<int[]>();
            
            @Override
            public void processFile(File f) {
                if (sVerboseMode) {
                    System.out.println("Adding size for file: " + f.getAbsolutePath());
                }
                long length = f.length();
                if ( calculateSlop && length > 0 ) {
                    int[] dirLen = mDirLen.peek();
                	long realLength = ((clusterSize-1)+length) / clusterSize*clusterSize;
                    long slop = realLength-length;
                    length += slop;
                	mSize[0] += length;
                    mSize[1] += slop;
                    dirLen[0] += f.getName().length()/13+3;
                } else {
                	mSize[0] += length;
                }
            }

            @Override
            public void processDirectory(File f) {
            	if ( calculateSlop ) {
            		int[] dirLen = new int[1];
                    dirLen[0] += f.getName().length()/13+4;
            		mDirLen.push(dirLen);
            	}
            }

			@Override
			public void endDirectory(File dir) {
            	if ( calculateSlop ) {
            		int[] dirLen = mDirLen.pop();
            		long lastDirLen = dirLen[0] * 32;
            		if ( lastDirLen != 0 ) {
                    	long realLength = ((clusterSize-1)+lastDirLen) / clusterSize*clusterSize;
                		long slop = realLength-lastDirLen;
                		mSize[0] += lastDirLen + slop;
                		mSize[1] += slop;
                		mSize[2] += lastDirLen;
            		}
            	}				
			}
        });
        System.out.println("Slop: " + mSize[1] + "   Directory Overhead: " + mSize[2] );
        return mSize[0];
    }

    // Process all files and directories under dir
    public static void processAllFiles(File dir, FileProcessor fp) {
        if (dir.isDirectory()) {
            fp.processDirectory(dir);
            String[] children = dir.list();
            for (int i = 0; i < children.length; i++) {
                processAllFiles(new File(dir, children[i]), fp);
            }
            fp.endDirectory(dir);
        } else {
            fp.processFile(dir);
        }
    }
}
