import java.io.*;
import java.util.*;

/** Basic driver program to be used as a shell for the MiniKernel for project 5.
 * It can be run in two modes:
 * <dl compact>
 *        <dt>Interactive:              <dd>java Boot ... FileTester
 *        <dt>With a test script file:  <dd>java Boot ... FileTester script
 * </dl>
 * To get a list of supported commands, type 'help' at the command prompt.
 * <p>
 * The testfile consists of commands to the driver program (one per line) as
 * well as comments.  Comments beginning with /* will be ignored completely by
 * the driver.  Comments beginning with // will be echoed to the output.
 * <p>
 * See the test files test*.data for examples.
 */
public class FileTester {
    /** Synopsis of commands. */
    private static String[] helpInfo = {
        "help",
        "quit",
        "format",
        "cd pathname",
        "create pathname",
        "read pathname",
        "write pathname data",
        "writeln pathname",
        "rm pathname",
        "mkdir pathname",
        "rmdir pathname",
        "ln oldpath newpath",
        "ls dirname",
    };

    /** Disk block size, as retrieved from the kernel. */
    private static int blockSize;

    /** A handy buffer to use for various purposes. */
    private static byte[] buf;

    /** A handy buffer to use for various purposes. */
    private static byte[] buf1;

    /** Main program.
     * @param args command-line arguments (there should be at most one:
     *      the name of a test file from which to read commands).
     */
    public static void main(String [] args){
        // NB:  This program is designed only to test the file system support
        // of the kernel, so it "cheats" in using non-kernel operations to
        // read commands and write diagnostics.
        if (args.length > 1) {
            System.err.println("usage: FileTester [ script-file ]");
            System.exit(0);
        }

        blockSize = Library.getDiskBlockSize();
        buf = new byte[blockSize];
        buf1 = new byte[blockSize];

        // Is the input coming from a file?
        boolean fromFile = (args.length == 1);

        // Create a stream for input
        BufferedReader input = null;

        // Open our input stream
        if (fromFile) {
            try {
                input = new BufferedReader(new FileReader(args[0]));
            } catch (FileNotFoundException e) {
                System.err.println("Error: Script file "
                        + args[0] + " not found.");
                System.exit(1);
            }
        } else {
            input = new BufferedReader(new InputStreamReader(System.in));
        }

        // Cycle through user or file input
        for (;;) {
            String cmd = null;
            try {
                // Print out the prompt for the user
                if (!fromFile) {
                    pr("--> ");
                    System.out.flush();
                }

                // Read in a line
                String line = input.readLine();

                // Check for EOF and empty lines
                if (line == null) {
                    // End of file (Ctrl-D for interactive input)
                    return;
                }
                line = line.trim();
                if (line.length() == 0) {
                    continue;
                }

                // Handle comments and echoing
                if (line.startsWith("//")) {
                    if (fromFile) {
                        pl(line);
                    }
                    continue;
                }
                if (line.startsWith("/*")) {
                    continue;
                }

                // Echo the command line
                if (fromFile) {
                    pl("--> " + line);
                }

                // Parse the command line
                StringTokenizer st = new StringTokenizer(line);
                cmd = st.nextToken();

                // Call the function that corresponds to the command
                int result = 0;
                if (cmd.equalsIgnoreCase("quit")) {
                    return;
                } else if (cmd.equalsIgnoreCase("help") || cmd.equals("?")) {
                    help();
                    continue;
                } else if (cmd.equalsIgnoreCase("format")) {
                    result = Library.format();
                } else if (cmd.equalsIgnoreCase("cd")) {
                    result = Library.chdir(st.nextToken());
                } else if (cmd.equalsIgnoreCase("create")) {
                    result = Library.create(st.nextToken());
                } else if (cmd.equalsIgnoreCase("read")) {
                    result = readTest(st.nextToken());
                } else if (cmd.equalsIgnoreCase("write")) {
                    result = writeTest(st.nextToken(), line);
                } else if (cmd.equalsIgnoreCase("writeln")) {
                    result = writeLines(st.nextToken(), input);
                } else if (cmd.equalsIgnoreCase("rm")) {
                    result = Library.delete(st.nextToken());
                } else if (cmd.equalsIgnoreCase("mkdir")) {
                    result = Library.mkdir(st.nextToken());
                } else if (cmd.equalsIgnoreCase("rmdir")) {
                    result = Library.rmdir(st.nextToken());
                } else if (cmd.equalsIgnoreCase("ln")) {
                    String oldName = st.nextToken();
                    String newName = st.nextToken();
                    result = Library.symlink(oldName, newName);
                } else if (cmd.equalsIgnoreCase("ls")) {
                    result = dumpDir(st.nextToken());
                } else {
                    pl("unknown command");
                    continue;
                }

                // Print out the result of the function call
                if (result != 0) {
                    if (result == -1) {
                        pl("*** System call failed");
                    } else {
                        pl("*** Bad result " + result + " from system call");
                    }
                }
            } catch (NoSuchElementException e) {
                // Handler for nextToken()
                pl("Incorrect number of arguments");
                help(cmd);
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }
        } // for (;;)
    } // main(String[])

    /** Prints a list of available commands. */
    private static void help() {
        pl("Commands are:");
        for (int i = 0; i < helpInfo.length; i++) {
            pl("    " + helpInfo[i]);
        }
    } // help()

    /** Prints help for command "cmd".
     * @param cmd the name of the command.
     */
    private static void help(String cmd) {
        for (int i = 0; i < helpInfo.length; i++) {
            if (helpInfo[i].startsWith(cmd)) {
                pl("usage: " + helpInfo[i]);
                return;
            }
        }
        pl("unknown command '" + cmd + "'");
    } // help(String)

    /** Reads data from a (simulated) file using Library.read
     * and displays the results.
     * @param fname the name of the file.
     * @return the result of the Library.read call.
     */
    private static int readTest(String fname) {
        int n = Library.read(fname, buf);
        boolean needNewline = false;
        if (n < 0) {
            return n;
        }
        for (int i = 0; i < buf.length; i++) {
            if (buf[i] != 0) {
                showChar(buf[i] & 0xff);
                needNewline = (buf[i] != '\n');
            }
        }
        if (needNewline) {
            pl("");
        }
        return n;
    } // readTest(String)

    /** Writes data to a (simulated) file using Library.write.
     * @param fname the name of the file.
     * @param info a source of data.
     * @return the result of the Library.write call.
     */
    private static int writeTest(String fname, String info) {
        // Info has the format 'write fname one two three ...
        int p;
        p = info.indexOf(' ');
        if (p >= 0) {
            p = info.indexOf(' ', p + 1);
        }
        if (p < 0) {
            p = 0;
        } else {
            p++;
        }
        Arrays.fill(buf, (byte)0);
        int i = 0;
        while (p < info.length()) {
            buf[i++] = (byte) info.charAt(p++);
        }
        return Library.write(fname, buf);
    } // writeTest(String, byte[])

    /** Write data to a (simulated) file using Library.write.
     * Data comes from the following lines in the input stream.
     * @param fname the name of the file.
     * @param in the input stream.
     * @return the result of the Library.write call.
     */
    private static int writeLines(String fname, BufferedReader in) {
        try {
            Arrays.fill(buf, (byte)0);
            int i = 0;
            for (;;) {
                String line = in.readLine();
                if (line == null || line.equals(".")) {
                    break;
                }
                for (int j = 0; j < line.length(); j++) {
                    if (i >= buf.length) {
                        break;
                    }
                    buf[i++] = (byte) line.charAt(j);
                }
                if (i >= buf.length) {
                    break;
                }
                buf[i++] = '\n';
            }
            return Library.write(fname, buf);
        } catch (IOException e) {
            e.printStackTrace();
            return -1;
        }
    } // writeLines(String, BufferedReader)

    /** Display a readable representation of a byte.
     * @param b the byte to display as a number in the range 0..255.
     */
    private static void showChar(int b) {
        if (b >= ' ' && b <= '~') {
            pr((char)b);
            return;
        }
        if (b == '\n') {
            pl("\\n");
            return;
        }
        if (b == '\\') {
            pr("\\\\");
            return;
        }
        pr('\\');
        pr(Integer.toString(b, 8));
    } // showChar(int)

    /** Displays the contents of a directory.
     * @param dirname the name of the directory.
     * @return the result of the readdir call.
     */
    private static int dumpDir(String dirname) {
        int n = Library.readdir(dirname, buf);
        if (n < 0) {
            return n;
        }
        for (int i = 0; i < buf.length; i += 16) {
            int block = ((buf[i] & 0xff) << 8) + (buf [i+1] & 0xff);
            if (block == 0) {
                continue;
            }
            StringBuffer sb = new StringBuffer();
            for (int j = 3; j < 16; j++) {
                if (buf[i + j] == 0) {
                    break;
                }
                sb.append((char) buf[i + j]);
            }
            String fname = sb.toString();
            pr(block + " " + fname);
            switch (buf[i + 2]) {
            case 0:
                break;
            case 1:
                pr("/");
                break;
            case 2:
                pr(" -> ");
                n = Library.readlink(dirname + "/" + fname, buf1);
                if (n < 0) {
                    return n;
                }
                for (int j = 0; j < buf1.length; j++) {
                    if (buf1[j] == 0) {
                        break;
                    }
                    pr("" + (char) buf1[j]);
                }
                break;
            default:
                pr("?" + buf[i + 2] + "?");
            }
            pl("");
        }
        return n;
    } // dumpDir(String)

    /** Prints a line to System.out followed by a newline.
     * @param o the message to print.
     */
    private static void pl(Object o) {
        System.out.println(o);
    } // pl(Object)

    /** Prints a line to System.out.
     * @param o the message to print.
     */
    private static void pr(Object o) {
        System.out.print(o);
    } // pl(Object)

    /** Prints a character to System.out.
     * @param c the character to print.
     */
    private static void pr(char c) {
        System.out.print(c);
    } // pl(char)
} // FileTester
