/*
 *   Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License").
 *   You may not use this file except in compliance with the License.
 *   A copy of the License is located at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   or in the "license" file accompanying this file. This file 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.amazon.pocketEtl.loader;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
import com.fasterxml.jackson.datatype.joda.JodaModule;

/**
 * A StringSerializer implementation for serializing a DTO to a delimited String with specified columnSeparator.
 * If a headerRow is used, this serializer is not thread-safe.
 *
 * PocketETL developer note: Currently it serializes joda datetime to string in UTC timezone. This behaviour can be
 * changed by setting default timezone using setTimeZone method of CsvMapper class.
 *
 * @param <T> Type of DTO to be deserialized.
 */
@SuppressWarnings("WeakerAccess")
public class CsvStringSerializer<T> implements StringSerializer<T> {
    private static final CsvMapper mapper = new CsvMapper();

    private static final char DEFAULT_COLUMN_SEPARATOR = ',';

    private final Class<T> classToSerialize;
    private final Character columnSeparator;
    private final Boolean writeHeaderRow;
    private ObjectWriter firstRowWriter;
    private ObjectWriter writer;

    private boolean hasWrittenFirstRow = false;

    /**
     * Standard constructor. Uses commas as a default columnSeparator.
     *
     * @param classToSerialize Class of DTO to be deserialized.
     */
    public static <T> CsvStringSerializer<T> of(Class<T> classToSerialize) {
        return new CsvStringSerializer<>(classToSerialize, null, null);
    }

    /**
     * Change the character to use to delimit columns that are being output.
     * @param columnSeparator Delimiter to be used as column separator.
     * @return A new modified CsvStringSerializer.
     */
    public CsvStringSerializer<T> withColumnSeparator(Character columnSeparator) {
        return new CsvStringSerializer<>(classToSerialize, columnSeparator, writeHeaderRow);
    }

    /**
     * Modify the behavior of this serializer to either start writing a header row the first time it is called or
     * stop doing so.
     * @param writeHeaderRow True to writer a header row the first time it is called; or false not to
     * @return A newly initialized copy of the existing object with this behavior changed.
     */
    public CsvStringSerializer<T> withHeaderRow(Boolean writeHeaderRow) {
        return new CsvStringSerializer<>(classToSerialize, columnSeparator, writeHeaderRow);
    }

    /**
     * Method for converting dto object to delimited string without quote character.
     *
     * @param objectToDeserialize Object to be deserialized.
     * @return Returns a delimited String.
     */
    @Override
    public String apply(T objectToDeserialize) {
        try {
            if (!hasWrittenFirstRow) {
                hasWrittenFirstRow = true;
                return firstRowWriter.writeValueAsString(objectToDeserialize);
            }

            return writer.writeValueAsString(objectToDeserialize);
        } catch (JsonProcessingException e){
            throw new RuntimeException(e);
        }
    }

    private CsvStringSerializer(Class<T> classToSerialize, Character columnSeparator, Boolean writeHeaderRow) {
        this.classToSerialize = classToSerialize;
        this.columnSeparator = columnSeparator;
        this.writeHeaderRow = writeHeaderRow;
        createObjectWriter();
    }

    private void createObjectWriter() {
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.registerModule(new JodaModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        CsvSchema schema = mapper.schemaFor(classToSerialize).withColumnSeparator(getColumnSeparator()).withoutQuoteChar();
        writer = mapper.writer(schema);

        if (getWriteHeaderRow()) {
            schema = schema.withHeader();
        }

        firstRowWriter = mapper.writer(schema);
    }

    private boolean getWriteHeaderRow() {
        return Boolean.TRUE.equals(writeHeaderRow);
    }

    private char getColumnSeparator() {
        return columnSeparator == null ? DEFAULT_COLUMN_SEPARATOR : columnSeparator;
    }
}