/*
 * 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.johnzon.jsonb.converter;

import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;

import javax.json.bind.annotation.JsonbDateFormat;

public class JsonbDateConverter extends JsonbDateConverterBase<Date> {
    private static final ZoneId UTC = ZoneId.of("UTC");

    // TODO: cheap impl to avoid to rely on exceptions, better can be to parse format
    //       -> rewrite it
    private volatile boolean hasTimezone = true;
    private volatile boolean isIso = false;

    public JsonbDateConverter(final JsonbDateFormat dateFormat) {
        super(dateFormat);
    }

    @Override
    public String toString(final Date instance) {
        return formatter == null ?
                Long.toString(instance.getTime()) :
                toStringWithFormatter(instance);
    }

    @Override
    public Date fromString(final String text) {
        return formatter == null ?
                new Date(Long.parseLong(text)) :
                fromStringWithFormatter(text);
    }

    private Date fromStringWithFormatter(final String text) {
        final boolean hasTimezone = this.hasTimezone;
        final boolean isIso = this.isIso;
        try {
            if (isIso) {
                return fromIso(text);
            }
            if (hasTimezone) {
                return fromZonedDateTime(text);
            }
            return fromLocalDateTime(text);
        } catch (final DateTimeException dte) {
            this.hasTimezone = !hasTimezone;
            try {
                if (hasTimezone) {
                    return fromLocalDateTime(text);
                }
                return fromZonedDateTime(text);
            } catch (final DateTimeException dte2) {
                final Date from = fromIso(text);
                this.isIso = !isIso;
                return from;
            }
        }
    }

    private Date fromIso(final String text) {
        return Date.from(ZonedDateTime.parse(text, DateTimeFormatter.ISO_ZONED_DATE_TIME).toInstant());
    }

    private String toStringWithFormatter(final Date instance) {
        final boolean hasTimezone = this.hasTimezone;
        final boolean isIso = this.isIso;
        final Instant instant = Instant.ofEpochMilli(instance.getTime());
        try {
            if (isIso) {
                return toIsoString(instant);
            }
            if (hasTimezone) {
                return toStringFromZonedDateTime(instant);
            }
            return toStringFromLocalDateTime(instant);
        } catch (final DateTimeException dte) {
            try {
                this.hasTimezone = !hasTimezone;
                if (hasTimezone) {
                    return toStringFromLocalDateTime(instant);
                }
                return toStringFromZonedDateTime(instant);
            } catch (final DateTimeException dte2) {
                this.isIso = !isIso;
                return toIsoString(instant);
            }
        }
    }

    private String toIsoString(final Instant instant) {
        return DateTimeFormatter.ISO_ZONED_DATE_TIME.format(ZonedDateTime.ofInstant(instant, UTC));
    }

    private Date fromLocalDateTime(final String text) {
        return Date.from(LocalDateTime.parse(text, formatter).toInstant(ZoneOffset.UTC));
    }

    private Date fromZonedDateTime(final String text) {
        final ZonedDateTime zonedDateTime = ZonedDateTime.parse(text, formatter);
        return Date.from(zonedDateTime.toInstant());
    }

    private String toStringFromLocalDateTime(final Instant instant) {
        return formatter.format(LocalDateTime.ofInstant(instant, UTC));
    }

    private String toStringFromZonedDateTime(final Instant instant) {
        return formatter.format(ZonedDateTime.ofInstant(instant, UTC));
    }
}
