package net.argius.filetool;

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

/**
 * ディレクトリ単位のディスク使用量計算処理。
 * 基本的にスレッドセーフではないが、
 * 同一オブジェクトを複数のスレッドから操作した場合、
 * <code>getSize</code> だけは上手く動作するかもしれない。
 */
public class DiskUsage {

    private PrintWriter out;
    private PrintWriter err;
    private boolean messageEnables;
    private int lower;
    private int upper;

    /**
     * DiskUsageの生成。
     */
    public DiskUsage() {
        this(new PrintWriter(System.out, true));
    }

    /**
     * DiskUsageの生成。
     * @param out 情報出力先
     */
    public DiskUsage(PrintWriter out) {
        this(out, new PrintWriter(System.err, true));
    }

    /**
     * DiskUsageの生成。
     * @param out 情報出力先
     * @param err メッセージ出力先
     */
    public DiskUsage(PrintWriter out, PrintWriter err) {
        this.out = out;
        this.err = err;
        this.messageEnables = false;
        this.lower = 0;
        this.upper = Integer.MAX_VALUE;
    }

    /**
     * メッセージ出力が有効かどうか調べる。
     * @return メッセージ出力が有効かどうか
     */
    public final boolean isMessageEnables() {
        return messageEnables;
    }

    /**
     * メッセージ出力が有効かどうかの設定。
     * @param enables メッセージ出力が有効かどうか
     */
    public final void setMessageEnables(boolean enables) {
        this.messageEnables = enables;
    }

    /**
     * 結果を出力する深さの範囲を設定する。
     * @param lower 閾値の下限値
     * @param upper 閾値の上限値
     */
    public final void setRange(int lower, int upper) {
        if (lower > upper || lower < 0 || upper < 0) {
            throw new IllegalArgumentException("range: " + lower + ',' + upper);
        }
        this.lower = lower;
        this.upper = upper;
    }

    /**
     * 情報の出力。
     * @param size サイズ
     * @param depth 深さ
     * @param file ファイル
     */
    public void printInfo(long size, int depth, File file) {
        StringBuffer buffer = new StringBuffer();
        int p1length = String.valueOf(size).length();
        if (p1length < 19) {
            buffer.append("                   ".substring(p1length));
        }
        buffer.append(size);
        buffer.append('\t');
        int p2length = String.valueOf(depth).length();
        if (p2length < 2) {
            buffer.append("  ".substring(p2length));
        }
        buffer.append(depth);
        buffer.append('\t');
        buffer.append(file.getPath());
        out.println(buffer);
    }

    /**
     * メッセージの出力。
     * @param object オブジェクト
     */
    public void printMessage(Object object) {
        if (messageEnables) {
            err.println(" ... scanning " + object);
        }
    }

    /**
     * 出力を有効にするかどうか調べる。
     * @param pw PrintWriter(out or err)
     * @param file 現在のファイル
     * @param depth 現在のファイルの深さ
     * @return 出力を有効にする場合は <code>true</code> 、そうでなければ <code>false</code>
     */
    protected boolean isOutputEnables(PrintWriter pw, File file, int depth) {
        if (pw == out && isInRange(depth)) {
            return true;
        } else if (pw == err) {
            return (upper - lower < 2) || (lower <= depth && depth < upper);
        }
        return false;
    }

    /**
     * 深さの取得。
     * @param file ファイル
     * @return 深さ
     */
    protected int getDepth(File file) {
        String path = file.getPath().replace(File.separatorChar, '/');
        if (path.endsWith(String.valueOf('/'))) {
            path = path.substring(0, path.length() - 1);
        }
        int count = 0;
        int index = 0;
        while (true) {
            index = path.indexOf('/', index);
            if (index >= 0) {
                ++count;
                ++index;
            } else {
                break;
            }
        }
        return count;
    }

    /**
     * 深さが範囲内かどうか調べる。
     * @param depth 深さ
     * @return 深さが範囲内なら <code>true</code> 、そうでなければ <code>false</code>
     */
    protected final boolean isInRange(int depth) {
        return (lower <= depth && depth <= upper);
    }

    /**
     * ディスク使用量情報の出力。
     * @param file 対象ファイル/ディレクトリ
     * @return 対象ファイル/ディレクトリのディスク使用量
     */
    public final long output(File file) {
        if (!file.isDirectory()) {
            return file.length();
        }
        int depth = getDepth(file);
        long size = 0;
        File[] files = file.listFiles();
        if (messageEnables && isOutputEnables(err, file, depth)) {
            printMessage(file);
        }
        if (files != null) {
            for (int i = 0; i < files.length; i++) {
                File child = files[i];
                size += output(child);
            }
        }
        if (isOutputEnables(out, file, depth)) {
            printInfo(size, depth, file);
        }
        return size;
    }

    /**
     * ディスク使用量の取得。
     * @param file 対象ファイル/ディレクトリ
     * @return 対象ファイル/ディレクトリのディスク使用量
     */
    public final long getSize(File file) {
        if (!file.isDirectory()) {
            return file.length();
        }
        long size = 0;
        File[] files = file.listFiles();
        if (files != null) {
            for (int i = 0; i < files.length; i++) {
                File child = files[i];
                size += getSize(child);
            }
        }
        return size;
    }

    /**
     * 処理を起動する。
     * @param args パラメータ
     */
    private static void invoke(String[] args) {
        DiskUsage du = new DiskUsage();
        du.setMessageEnables(true);
        du.setRange(0, 3);
        boolean onlyTotal = false;
        List pathList = new ArrayList();
        for (int i = 0; i < args.length; i++) {
            String arg = args[i];
            if (arg.startsWith("-")) {
                if (arg.equals("-q") || arg.equals("--quit")) {
                    du.setMessageEnables(false);
                } else if (arg.equals("--total")) {
                    onlyTotal = true;
                } else if (arg.equals("-r") || arg.equals("--range")) {
                    i += 2;
                    if (args.length <= i) {
                        throw new IllegalArgumentException("invalid option: range requires 2 parameters");
                    }
                    int lower;
                    int upper;
                    try {
                        lower = Integer.parseInt(args[i - 1]);
                        upper = Integer.parseInt(args[i]);
                    } catch (NumberFormatException ex) {
                        throw new IllegalArgumentException("invalid range parameter: "
                                                           + ex.getMessage());
                    }
                    du.setRange(lower, upper);
                } else {
                    throw new IllegalArgumentException("invalid option: " + arg);
                }
            } else {
                pathList.add(arg);
            }
        }
        // main
        for (int i = 0, n = pathList.size(); i < n; i++) {
            File file = new File(((String)pathList.get(i)));
            if (onlyTotal) {
                long size = du.getSize(file);
                StringBuffer buffer = new StringBuffer();
                int p1length = String.valueOf(size).length();
                if (p1length < 19) {
                    buffer.append("                   ".substring(p1length));
                }
                buffer.append(size);
                buffer.append('\t');
                buffer.append(file.getPath());
                System.out.println(buffer);
            } else {
                du.output(file);
            }
        }
    }

    /**
     * 使い方の表示。
     */
    private static void printUsage() {
        PrintStream err = System.err;
        err.println("DiskUsage (argius-filetool)");
        err.println("usage : (command) [path...]");
        err.println("options: ");
        err.println("  -q, --quit   suppress messages");
        err.println("  -r, --range  setting range to show infos and messages");
        err.println("      --total  do print total size only");
        err.println("  -h, --help   show (this) help");
    }

    /**
     * DiskUsageの実行。
     * @param args パラメータ(see 'usage')
     */
    public static void main(String[] args) {
        if (args.length == 0) {
            printUsage();
            return;
        }
        for (int i = 0; i < args.length; i++) {
            String arg = args[i];
            if (arg.equals("-h") || arg.equals("--help")) {
                printUsage();
                return;
            }
        }
        int status;
        try {
            invoke(args);
            status = 0;
        } catch (Exception ex) {
            String message = ex + Utilities.getFirstStackTrace(ex, DiskUsage.class);
            System.out.println("DiskUsage: " + message);
            status = 255;
        }
        if (status > 0) {
            System.exit(status);
        }
    }

}
