/*
 *  ******************************************************************************
 *  *
 *  *
 *  * This program and the accompanying materials are made available under the
 *  * terms of the Apache License, Version 2.0 which is available at
 *  * https://www.apache.org/licenses/LICENSE-2.0.
 *  *
 *  *  See the NOTICE file distributed with this work for additional
 *  *  information regarding copyright ownership.
 *  * 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.
 *  *
 *  * SPDX-License-Identifier: Apache-2.0
 *  *****************************************************************************
 */

package org.datavec.api.transform.transform.time;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Pattern;

import org.datavec.api.transform.metadata.ColumnMetaData;
import org.datavec.api.transform.metadata.TimeMetaData;
import org.datavec.api.transform.transform.BaseColumnTransform;
import org.datavec.api.writable.LongWritable;
import org.datavec.api.writable.Writable;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.nd4j.shade.jackson.annotation.JsonIgnoreProperties;
import org.nd4j.shade.jackson.annotation.JsonProperty;

import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(exclude = { "formatter", "formatters" })
@JsonIgnoreProperties({ "formatters", "formatter" })
public class StringToTimeTransform extends BaseColumnTransform {

	private final String timeFormat;
	private final DateTimeZone timeZone;
	private final Locale locale;
	private final Long minValidTime;
	private final Long maxValidTime;
	// formats from:
	// http://www.java2s.com/Tutorials/Java/Data_Type_How_to/Legacy_Date_Format/Guess_the_format_pattern_based_on_date_value.htm
	// 2017-09-21T17:06:29.064687
	// 12/1/2010 11:21
	private static final String[] formats = { "YYYY-MM-dd'T'HH:mm:ss", "YYYY-MM-dd", "YYYY-MM-dd'T'HH:mm:ss'Z'",
			"YYYY-MM-dd'T'HH:mm:ssZ", "YYYY-MM-dd'T'HH:mm:ss.SSS'Z'", "YYYY-MM-dd'T'HH:mm:ss.SSSZ",
			"YYYY-MM-dd HH:mm:ss", "MM/dd/YYYY HH:mm:ss", "MM/dd/YYYY'T'HH:mm:ss.SSS'Z'", "MM/dd/YYYY'T'HH:mm:ss.SSSZ",
			"MM/dd/YYYY'T'HH:mm:ss.SSS", "MM/dd/YYYY'T'HH:mm:ssZ", "MM/dd/YYYY'T'HH:mm:ss", "YYYY:MM:dd HH:mm:ss",
			"YYYYMMdd", "YYYY-MM-dd HH:mm:ss", "MM/dd/YYYY HH:mm",

	};
	private transient DateTimeFormatter[] formatters;

	private transient DateTimeFormatter formatter;

	/**
	 * Instantiate this without a time format specified. If this constructor is
	 * used, this transform will be allowed to handle several common transforms as
	 * defined in the static formats array.
	 *
	 *
	 * @param columnName Name of the String column
	 * @param timeZone   Timezone for time parsing
	 */
	public StringToTimeTransform(String columnName, TimeZone timeZone) {
		this(columnName, null, timeZone, null, null, null);
	}

	/**
	 * @param columnName Name of the String column
	 * @param timeZone   Timezone for time parsing
	 * @param locale     Locale for i18n
	 */
	public StringToTimeTransform(String columnName, TimeZone timeZone, Locale locale) {
		this(columnName, null, timeZone, locale, null, null);
	}

	/**
	 * @param columnName Name of the String column
	 * @param timeFormat Time format, as per <a href=
	 *                   "http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html">http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html</a>
	 * @param timeZone   Timezone for time parsing
	 */
	public StringToTimeTransform(String columnName, String timeFormat, TimeZone timeZone) {
		this(columnName, timeFormat, timeZone, null, null, null);
	}

	/**
	 * @param columnName Name of the String column
	 * @param timeFormat Time format, as per <a href=
	 *                   "http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html">http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html</a>
	 * @param timeZone   Timezone for time parsing
	 * @param locale     Locale for i18n
	 */
	public StringToTimeTransform(String columnName, String timeFormat, TimeZone timeZone, Locale locale) {
		this(columnName, timeFormat, timeZone, locale, null, null);
	}

	/**
	 * Instantiate this without a time format specified. If this constructor is
	 * used, this transform will be allowed to handle several common transforms as
	 * defined in the static formats array.
	 *
	 *
	 * @param columnName Name of the String column
	 * @param timeZone   Timezone for time parsing
	 * @param locale     Locale for i18n
	 */
	public StringToTimeTransform(String columnName, DateTimeZone timeZone, Locale locale) {
		this(columnName, null, timeZone, locale, null, null);
	}

	/**
	 * @param columnName Name of the String column
	 * @param timeFormat Time format, as per <a href=
	 *                   "http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html">http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html</a>
	 * @param timeZone   Timezone for time parsing
	 */
	public StringToTimeTransform(String columnName, String timeFormat, DateTimeZone timeZone) {
		this(columnName, timeFormat, timeZone, null, null, null);
	}

	/**
	 * @param columnName Name of the String column
	 * @param timeFormat Time format, as per <a href=
	 *                   "http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html">http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html</a>
	 * @param timeZone   Timezone for time parsing
	 * @param locale     Locale for i18n
	 */
	public StringToTimeTransform(String columnName, String timeFormat, DateTimeZone timeZone, Locale locale) {
		this(columnName, timeFormat, timeZone, locale, null, null);
	}

	/**
	 * @param columnName   Name of the String column
	 * @param timeFormat   Time format, as per <a href=
	 *                     "http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html">http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html</a>
	 * @param timeZone     Timezone for time parsing
	 * @param locale       Locale for i18n
	 * @param minValidTime Min valid time (epoch millisecond format). If null: no
	 *                     restriction in min valid time
	 * @param maxValidTime Max valid time (epoch millisecond format). If null: no
	 *                     restriction in max valid time
	 */
	public StringToTimeTransform(@JsonProperty("columnName") String columnName,
			@JsonProperty("timeFormat") String timeFormat, @JsonProperty("timeZone") TimeZone timeZone,
			@JsonProperty("locale") Locale locale, @JsonProperty("minValidTime") Long minValidTime,
			@JsonProperty("maxValidTime") Long maxValidTime) {
		this(columnName, timeFormat, DateTimeZone.forTimeZone(timeZone), locale, minValidTime, maxValidTime);
	}

	/**
	 * @param columnName   Name of the String column
	 * @param timeFormat   Time format, as per <a href=
	 *                     "http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html">http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html</a>
	 * @param timeZone     Timezone for time parsing
	 * @param locale       Locale for i18n
	 * @param minValidTime Min valid time (epoch millisecond format). If null: no
	 *                     restriction in min valid time
	 * @param maxValidTime Max valid time (epoch millisecond format). If null: no
	 *                     restriction in max valid time
	 */
	public StringToTimeTransform(String columnName, String timeFormat, DateTimeZone timeZone, Locale locale,
			Long minValidTime, Long maxValidTime) {
		super(columnName);
		this.timeFormat = timeFormat;
		this.timeZone = timeZone;
		this.locale = locale;
		this.minValidTime = minValidTime;
		this.maxValidTime = maxValidTime;
		if (timeFormat != null)
			if (locale != null) {
				this.formatter = DateTimeFormat.forPattern(timeFormat).withZone(timeZone).withLocale(locale);
			} else {
				this.formatter = DateTimeFormat.forPattern(timeFormat).withZone(timeZone);
			}
		else {
			List<DateTimeFormatter> dateFormatList = new ArrayList<>();
			formatters = new DateTimeFormatter[formats.length];
			for (int i = 0; i < formatters.length; i++) {
				if (locale != null) {
					dateFormatList.add(DateTimeFormat.forPattern(formats[i]).withZone(timeZone).withLocale(locale));
				} else {
					dateFormatList.add(DateTimeFormat.forPattern(formats[i]).withZone(timeZone));
				}
			}
			formatters = dateFormatList.toArray(new DateTimeFormatter[dateFormatList.size()]);
		}
	}

	@Override
	public ColumnMetaData getNewColumnMetaData(String newName, ColumnMetaData oldColumnType) {
		return new TimeMetaData(newName, timeZone, minValidTime, maxValidTime);
	}

	@Override
	public Writable map(Writable columnWritable) {
		String str = columnWritable.toString().trim();
		if (str.contains("'T'")) {
			str = str.replaceFirst("'T'", "T");
		}

		if (formatter == null) {
			long result = -1;
			if (Pattern.compile("\\.[0-9]+").matcher(str).find()) {
				str = str.replaceAll("\\.[0-9]+", "");
			}

			for (DateTimeFormatter formatter : formatters) {
				try {
					result = formatter.parseMillis(str);
					return new LongWritable(result);
				} catch (Exception e) {

				}

			}

			if (result < 0) {
				throw new IllegalStateException("Unable to parse date time " + str);
			}
		} else {
			long time = formatter.parseMillis(str);
			return new LongWritable(time);
		}

		throw new IllegalStateException("Unable to parse date time " + str);

	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("StringToTimeTransform(timeZone=").append(timeZone);
		if (minValidTime != null)
			sb.append(",minValidTime=").append(minValidTime);
		if (maxValidTime != null) {
			if (minValidTime != null)
				sb.append(",");
			sb.append("maxValidTime=").append(maxValidTime);
		}
		sb.append(")");
		return sb.toString();
	}

	// Custom serialization methods, because Joda Time doesn't allow
	// DateTimeFormatter objects to be serialized :(
	private void writeObject(ObjectOutputStream out) throws IOException {
		out.defaultWriteObject();
	}

	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
		in.defaultReadObject();
		if (timeFormat != null)
			if (locale != null) {
				formatter = DateTimeFormat.forPattern(timeFormat).withZone(timeZone).withLocale(locale);
			} else {
				formatter = DateTimeFormat.forPattern(timeFormat).withZone(timeZone);
			}
		else {
			List<DateTimeFormatter> dateFormatList = new ArrayList<>();
			formatters = new DateTimeFormatter[formats.length];
			for (int i = 0; i < formatters.length; i++) {
				dateFormatList.add(DateTimeFormat.forPattern(formats[i]).withZone(timeZone));
			}

			formatters = dateFormatList.toArray(new DateTimeFormatter[dateFormatList.size()]);
		}
	}

	/**
	 * Transform an object in to another object
	 *
	 * @param input the record to transform
	 * @return the transformed writable
	 */
	@Override
	public Object map(Object input) {
		return null;
	}
}
