package sample.context.report.csv;

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

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;

import lombok.*;
import sample.InvocationException;

/**
 * CSVの書き出し処理をサポートするユーティリティ。
 */
@Data
@AllArgsConstructor
public class CsvWriter {

    private File file;
    private OutputStream out;
    private CsvLayout layout = new CsvLayout();

    public static CsvWriter of(final File file) {
        return new CsvWriter(file, null, new CsvLayout());
    }

    public static CsvWriter of(final File file, final CsvLayout layout) {
        return new CsvWriter(file, null, layout);
    }

    public static CsvWriter of(final OutputStream out) {
        return new CsvWriter(null, out, new CsvLayout());
    }

    public static CsvWriter of(final OutputStream out, final CsvLayout layout) {
        return new CsvWriter(null, out, layout);
    }

    /** ファイルリソース経由での読み込み時にtrue */
    public boolean fromFile() {
        return file != null;
    }

    /**
     * CSV書出処理(上書き)を行います。
     * <p>CsvWrite#appendRow 呼び出すタイミングでファイルへ随時書き出しが行われます。
     * @param logic
     */
    public void write(final CsvWrite logic) {
        OutputStream out = null;
        try {
            out = fromFile() ? FileUtils.openOutputStream(file) : this.out;
            CsvStream stream = new CsvStream(layout, out);
            logic.execute(stream);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new InvocationException(e);
        } finally {
            if (fromFile()) {
                closeQuietly(out);
            }
        }
    }
    
    private void closeQuietly(final Closeable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (final IOException ioe) {
        }
    }

    /**
     * CSV書出処理(追記)を行います。
     * <p>CsvWrite#appendRow 呼び出すタイミングでファイルへ随時書き出しが行われます。
     * <p>ファイル出力時のみ利用可能です。
     * @param logic
     */
    public void writeAppend(final CsvWrite logic) {
        if (!fromFile())
            throw new UnsupportedOperationException("CSV書出処理の追記はファイル出力時のみサポートされます");
        FileOutputStream out = null;
        try {
            out = FileUtils.openOutputStream(file, true);
            CsvStream stream = new CsvStream(layout, out);
            logic.execute(stream);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new InvocationException(e);
        } finally {
            closeQuietly(out);
        }
    }

    public static class CsvStream {
        private CsvLayout layout;
        private OutputStream out;

        public CsvStream(CsvLayout layout, OutputStream out) {
            this.layout = layout;
            this.out = out;
            if (layout.hasHeader()) {
                appendRow(layout.headerCols());
            }
        }

        public CsvStream appendRow(List<Object> cols) {
            try {
                out.write(row(cols).getBytes(layout.getCharset()));
                out.write(layout.getEolSymbols().getBytes());
                return this;
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new IllegalArgumentException(e);
            }
        }

        public String row(List<Object> cols) {
            List<String> row = new ArrayList<>();
            for (Object col : cols) {
                if (col instanceof String) {
                    row.add(escape(col.toString()));
                } else {
                    if (col == null) {
                        row.add("");
                    } else {
                        row.add(col.toString());
                    }
                }
            }
            return StringUtils.join(row, ",");
        }

        private String escape(String s) {
            if (layout.isNonQuote()) {
                return s;
            }
            char delim = layout.getDelim();
            char quote = layout.getQuote();
            String quoteStr = String.valueOf(quote);
            String eol = layout.getEolSymbols();
            if (StringUtils.containsNone(s, delim, quote) && StringUtils.containsNone(s, eol)) {
                return quoteStr + s + quoteStr;
            } else {
                return quoteStr + StringUtils.replace(s, quoteStr, quoteStr + quoteStr) + quoteStr;
            }
        }
    }

    /** CSV出力処理を表現します。  */
    public static interface CsvWrite {
        /**
         * @param stream 出力CSVインスタンス
         */
        void execute(final CsvStream stream);
    }

}