/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.myfaces.tobago.convert;

import org.apache.myfaces.tobago.internal.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.convert.ConverterException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;

import static org.apache.myfaces.tobago.convert.DateTimeConverter.CONVERTER_ID;

@org.apache.myfaces.tobago.apt.annotation.Converter(id = CONVERTER_ID)
public class DateTimeConverter extends javax.faces.convert.DateTimeConverter {

  private static final Logger LOG = LoggerFactory.getLogger(DateTimeConverter.class);

  public static final String CONVERTER_ID = "org.apache.myfaces.tobago.DateTime";

  private static final String TYPE_DATE = "date";
  private static final String TYPE_TIME = "time";
  private static final String TYPE_BOTH = "both";
  private static final String TYPE_CALENDAR = "calendar";
  private static final String TYPE_LOCAL_DATE = "localDate";
  private static final String TYPE_LOCAL_TIME = "localTime";
  private static final String TYPE_LOCAL_DATE_TIME = "localDateTime";
  private static final String TYPE_OFFSET_TIME = "offsetTime";
  private static final String TYPE_OFFSET_DATE_TIME = "offsetDateTime";
  private static final String TYPE_ZONED_DATE_TIME = "zonedDateTime";

  @Override
  public Object getAsObject(FacesContext facesContext, UIComponent component, String string) throws ConverterException {
    if (StringUtils.isBlank(string)) {
      return null;
    } else {
      final String type = getType();
      if (TYPE_DATE.equals(type) || TYPE_TIME.equals(type) || TYPE_BOTH.equals(type)) {
        return super.getAsObject(facesContext, component, string);
      } else if (TYPE_CALENDAR.equals(type)) {
        final Locale locale = getLocale();
        final String pattern = getPattern();
        final TimeZone timeZone = getTimeZone();
        final Calendar calendar;
        if (component instanceof UIInput && ((UIInput) component).getValue() != null) {
          calendar = (Calendar) ((UIInput) component).getValue();
        } else {
          if (timeZone != null && locale != null) {
            calendar = Calendar.getInstance(timeZone, locale);
          } else if (locale != null) {
            calendar = Calendar.getInstance(locale);
          } else if (timeZone != null) {
            calendar = Calendar.getInstance(timeZone);
          } else {
            calendar = Calendar.getInstance();
          }
        }

        SimpleDateFormat sdf = locale != null ? new SimpleDateFormat(pattern, locale) : new SimpleDateFormat(pattern);
        try {
          calendar.setTime(sdf.parse(string));
        } catch (ParseException e) {
          throw new ConverterException("string='" + string + "'", e);
        }

        return calendar;
      } else if (TYPE_LOCAL_DATE.equals(type)) {
        logMandatoryPatterns("yMd");
        return getDateTimeFormatter().parse(string, LocalDate::from);
      } else if (TYPE_LOCAL_TIME.equals(type)) {
        logMandatoryPatterns("H");
        return getDateTimeFormatter().parse(string, LocalTime::from);
      } else if (TYPE_LOCAL_DATE_TIME.equals(type)) {
        logMandatoryPatterns("yMdH");
        return getDateTimeFormatter().parse(string, LocalDateTime::from);
      } else if (TYPE_OFFSET_TIME.equals(type)) {
        logMandatoryPatterns("HZ");
        return getDateTimeFormatter().parse(string, OffsetTime::from);
      } else if (TYPE_OFFSET_DATE_TIME.equals(type)) {
        logMandatoryPatterns("yMdHZ");
        return getDateTimeFormatter().parse(string, OffsetDateTime::from);
      } else if (TYPE_ZONED_DATE_TIME.equals(type)) {
        logMandatoryPatterns("yMdHZ");
        return getDateTimeFormatter().parse(string, ZonedDateTime::from);
      } else {
        throw new ConverterException("invalid type '" + type + "'");
      }
    }
  }

  @Override
  public String getAsString(FacesContext facesContext, UIComponent component, Object object) throws ConverterException {
    if (object == null) {
      return null;
    } else {
      final String type = getType();
      if (TYPE_DATE.equals(type) || TYPE_TIME.equals(type) || TYPE_BOTH.equals(type)) {
        return super.getAsString(facesContext, component, object);
      } else if (TYPE_CALENDAR.equals(type)) {
        Calendar calendar = (Calendar) object;
        final Locale locale = getLocale();
        final String pattern = getPattern();

        SimpleDateFormat sdf = new SimpleDateFormat(pattern, locale);
        return sdf.format(calendar.getTime());
      } else if (TYPE_LOCAL_DATE.equals(type)
          || TYPE_LOCAL_TIME.equals(type)
          || TYPE_LOCAL_DATE_TIME.equals(type)
          || TYPE_OFFSET_TIME.equals(type)
          || TYPE_OFFSET_DATE_TIME.equals(type)
          || TYPE_ZONED_DATE_TIME.equals(type)) {
        return getDateTimeFormatter().format((TemporalAccessor) object);
      } else {
        throw new ConverterException("invalid type '" + type + "'");
      }
    }
  }

  private void logMandatoryPatterns(String mandatoryChars) {
    logMandatoryPattern(mandatoryChars, "y", "year");
    logMandatoryPattern(mandatoryChars, "M", "month");
    logMandatoryPattern(mandatoryChars, "d", "day");
    logMandatoryPattern(mandatoryChars, "H", "hour");
    logMandatoryPattern(mandatoryChars, "Z", "offset");
  }

  private void logMandatoryPattern(String mandatoryChars, String c, String name) {
    final String pattern = getPattern();
    if (pattern != null && mandatoryChars.contains(c) && !pattern.contains(c)) {
      LOG.error("No char for " + name + " ('" + c + "') in pattern: " + pattern);
    }
  }

  private DateTimeFormatter getDateTimeFormatter() {
    final String pattern = getPattern();
    if (!StringUtils.isBlank(pattern)) {
      final Locale locale = getLocale();
      return locale != null ? DateTimeFormatter.ofPattern(pattern, locale) : DateTimeFormatter.ofPattern(pattern);
    } else {
      throw new ConverterException("no pattern set");
    }
  }
}
