/*
 * Copyright 2016 Andrew Rucker Jones.
 *
 * 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.opencsv.bean;

import com.opencsv.CSVWriter;
import com.opencsv.ICSVParser;
import com.opencsv.ICSVWriter;
import com.opencsv.bean.exceptionhandler.CsvExceptionHandler;
import com.opencsv.bean.exceptionhandler.ExceptionHandlerQueue;
import com.opencsv.bean.exceptionhandler.ExceptionHandlerThrow;
import org.apache.commons.collections4.ListValuedMap;
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
import org.apache.commons.lang3.ObjectUtils;

import java.io.Writer;
import java.lang.reflect.Field;
import java.util.Locale;
import java.util.ResourceBundle;

/**
 * This is a builder for StatefulBeanToCsv, allowing one to set all parameters
 * necessary for writing a CSV file.
 * 
 * @param <T> The type of the beans being written
 * @author Andrew Rucker Jones
 * @since 3.9
 */
public class StatefulBeanToCsvBuilder<T> {

    private char separator = ICSVWriter.DEFAULT_SEPARATOR;
    private char quotechar = ICSVWriter.DEFAULT_QUOTE_CHARACTER;
    private char escapechar = ICSVWriter.DEFAULT_ESCAPE_CHARACTER;
    private String lineEnd = CSVWriter.DEFAULT_LINE_END;
    private MappingStrategy<T> mappingStrategy = null;
    private final Writer writer;
    private final ICSVWriter csvWriter;
    private CsvExceptionHandler exceptionHandler = new ExceptionHandlerThrow();
    private boolean orderedResults = true;
    private Locale errorLocale = Locale.getDefault();
    private boolean applyQuotesToAll = true;
    private final ListValuedMap<Class<?>, Field> ignoredFields = new ArrayListValuedHashMap<>();
    
    /**
     * Default constructor - Being stateful the writer is required by the builder at the start and not added in later.
     *
     * @param writer - the writer that will be used to output the csv version of the bean.
     */
    public StatefulBeanToCsvBuilder(Writer writer) {
        this.writer = writer;
        this.csvWriter = null;
    }

    /**
     * Being stateful the writer is required by the builder at the start and not added in later.
     * By passing in the ICSVWriter you can create a writer with the desired ICSVParser to allow you to
     * use the exact same parser for reading and writing.
     *
     * @param icsvWriter - the ICSVWriter that will be used to output the csv version of the bean.
     * @since 4.2
     */
    public StatefulBeanToCsvBuilder(ICSVWriter icsvWriter) {
        this.writer = null;
        this.csvWriter = icsvWriter;
    }
    
    /**
     * Sets the mapping strategy for writing beans to a CSV destination.
     * <p>If the mapping strategy is set this way, it will always be used instead
     * of automatic determination of an appropriate mapping strategy.</p>
     * <p>It is perfectly legitimate to read a CSV source, take the mapping
     * strategy from the read operation, and pass it in to this method for a
     * write operation. This conserves some processing time, but, more
     * importantly, preserves header ordering.</p>
     * 
     * @param mappingStrategy The mapping strategy to be used for write operations
     * @return this
     */
    public StatefulBeanToCsvBuilder<T> withMappingStrategy(MappingStrategy<T> mappingStrategy) {
        this.mappingStrategy = mappingStrategy;
        return this;
    }
    
    /**
     * @see com.opencsv.CSVWriter#separator
     * @param separator The field separator to be used when writing a CSV file
     * @return this
     */
    public StatefulBeanToCsvBuilder<T> withSeparator(char separator) {
        this.separator = separator;
        return this;
    }
    
    /**
     * @see com.opencsv.CSVWriter#quotechar
     * @param quotechar The quote character to be used when writing a CSV file
     * @return this
     */
    public StatefulBeanToCsvBuilder<T> withQuotechar(char quotechar) {
        this.quotechar = quotechar;
        return this;
    }
    
    /**
     * @see com.opencsv.CSVWriter#escapechar
     * @param escapechar The escape character to be used when writing a CSV file
     * @return this
     */
    public StatefulBeanToCsvBuilder<T> withEscapechar(char escapechar) {
        this.escapechar = escapechar;
        return this;
    }
    
    /**
     * @see com.opencsv.CSVWriter#lineEnd
     * @param lineEnd The line ending to be used when writing a CSV file
     * @return this
     */
    public StatefulBeanToCsvBuilder<T> withLineEnd(String lineEnd) {
        this.lineEnd = lineEnd;
        return this;
    }

    /**
     * Sets the handler for recoverable exceptions that arise during the
     * processing of records.
     * <p>This is a convenience function and is maintained for backwards
     * compatibility. Passing in {@code true} is equivalent to
     * {@code withExceptionHandler(new ExceptionHandlerThrow())}
     * and {@code false} is equivalent to
     * {@code withExceptionHandler(new ExceptionHandlerQueue())}</p>
     * <p>Please note that if both this method and
     * {@link #withExceptionHandler(CsvExceptionHandler)} are called,
     * the last call wins.</p>
     * @see #withExceptionHandler(CsvExceptionHandler)
     * @param throwExceptions Whether or not exceptions should be thrown while
     *   writing a CSV file. If not, they may be retrieved later by calling
     *   {@link com.opencsv.bean.StatefulBeanToCsv#getCapturedExceptions() }.
     * @return this
     */
    public StatefulBeanToCsvBuilder<T> withThrowExceptions(boolean throwExceptions) {
        if(throwExceptions) {
            exceptionHandler = new ExceptionHandlerThrow();
        }
        else {
            exceptionHandler = new ExceptionHandlerQueue();
        }
        return this;
    }

    /**
     * Sets the handler for recoverable exceptions raised during processing of
     * records.
     * <p>If neither this method nor {@link #withThrowExceptions(boolean)} is
     * called, the default exception handler is
     * {@link ExceptionHandlerThrow}.</p>
     * <p>Please note that if both this method and
     * {@link #withThrowExceptions(boolean)} are called, the last call wins.</p>
     *
     * @param exceptionHandler The exception handler to be used. If {@code null},
     *                this method does nothing.
     * @return {@code this}
     * @since 5.2
     */
    public StatefulBeanToCsvBuilder<T> withExceptionHandler(CsvExceptionHandler exceptionHandler) {
        if(exceptionHandler != null) {
            this.exceptionHandler = exceptionHandler;
        }
        return this;
    }
    
    /**
     * Sets whether or not results must be written in the same order in which
     * they appear in the list of beans provided as input.
     * 
     * @param orderedResults Whether or not the lines written are in the same
     *   order they appeared in the input
     * @return this
     * @see StatefulBeanToCsv#setOrderedResults(boolean)
     * @since 4.0
     */
    public StatefulBeanToCsvBuilder<T> withOrderedResults(boolean orderedResults) {
        this.orderedResults = orderedResults;
        return this;
    }
    
    /**
     * Sets the locale to be used for all error messages.
     * @param errorLocale Locale for error messages. If null, the default locale
     *   is used.
     * @return this
     * @see StatefulBeanToCsv#setErrorLocale(java.util.Locale) 
     * @since 4.0
     */
    public StatefulBeanToCsvBuilder<T> withErrorLocale(Locale errorLocale) {
        this.errorLocale = ObjectUtils.defaultIfNull(errorLocale, Locale.getDefault());
        return this;
    }

    /**
     * Sets whether all outputs should be put in quotes.
     * Defaults to {@code true}.
     *
     * @param applyQuotesToAll Whether all outputs should be quoted
     * @return this
     * @see com.opencsv.CSVWriter#writeNext(String[], boolean)
     * @since 4.2
     */
    public StatefulBeanToCsvBuilder<T> withApplyQuotesToAll(boolean applyQuotesToAll) {
        this.applyQuotesToAll = applyQuotesToAll;
        return this;
    }

    /**
     * Adds a {@link Field} to the list of fields opencsv should ignore
     * completely.
     * <p>May be called as many times as necessary.</p>
     * @param type The class opencsv will encounter the field in during
     *             processing. In the case of inheritance, this may not be the
     *             declaring class.
     * @param field The field opencsv is to ignore
     * @return {@code this}
     * @throws IllegalArgumentException If one of the parameters is
     * {@code null} or {@code field} cannot be found in {@code type}.
     * @since 5.0
     * @see MappingStrategy#ignoreFields(MultiValuedMap)
     */
    public StatefulBeanToCsvBuilder<T> withIgnoreField(Class<?> type, Field field) throws IllegalArgumentException {
        if(type != null && field != null && field.getDeclaringClass().isAssignableFrom(type)) {
            ignoredFields.put(type, field);
        }
        else {
            throw new IllegalArgumentException(ResourceBundle.getBundle(
                    ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
                    .getString("ignore.field.inconsistent"));
        }
        return this;
    }
    
    /**
     * Builds a StatefulBeanToCsv from the information provided, filling in
     * default values where none have been specified.
     * @return A new {@link StatefulBeanToCsv}
     */
    public StatefulBeanToCsv<T> build() {
        StatefulBeanToCsv<T> sbtcsv;
        if (writer != null) {
            sbtcsv = new StatefulBeanToCsv<>(escapechar, lineEnd,
                    mappingStrategy, quotechar, separator, exceptionHandler,
                    writer, applyQuotesToAll, ignoredFields);
        } else {
            sbtcsv = new StatefulBeanToCsv<>(mappingStrategy, exceptionHandler,
                    applyQuotesToAll, csvWriter, ignoredFields);
        }

        sbtcsv.setOrderedResults(orderedResults);
        sbtcsv.setErrorLocale(errorLocale);
        return sbtcsv;
    }
}
