package com.atlassian.stash.internal.web.timezone;

import com.atlassian.fugue.Pair;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.TreeMultimap;

import javax.annotation.Nonnull;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.TextStyle;
import java.util.*;

import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;

/**
 * Helper class for serializing IANA Time Zone Database {@link ZoneId zone IDs}, including a localized
 * {@link ZoneId#getDisplayName(TextStyle, Locale) display name} and GMT offset.
 */
class LocalizedZoneCollection {

    private static final String GMT = "GMT";

    private final List<Region> regions;

    LocalizedZoneCollection(@Nonnull Locale locale) {
        TreeMultimap<String, LocalizedTimeZone> regionMap = TreeMultimap.create();

        ZoneId.getAvailableZoneIds().stream()
                .map(ZoneId::of)
                // "Etc/" time zones are really confusing; they expose GMT offsets, but flip the +/- offset.
                // See http://opensource.apple.com/source/system_cmds/system_cmds-230/zic.tproj/datfiles/etcetera
                .filter(zoneId -> !zoneId.getId().startsWith("Etc/"))
                .forEach(zoneId -> getRegion(zoneId).ifPresent(regionPlace ->
                        regionMap.put(regionPlace.left(), timeZone(zoneId, locale, regionPlace.right()))));

        // Manually add the GMT offsets, they are not contained in the available zone ids.
        for (int i = -12; i <= 12; i++) {
            String offsetLabel = (i == 0 ? "" : i > 0 ? "+" : "") + i;
            ZoneId zoneId = ZoneId.of(GMT + offsetLabel);
            regionMap.put(GMT, timeZone(zoneId, locale));
        }

        regions = regionMap.asMap().entrySet().stream()
                .map(entry -> new Region(entry.getKey(), entry.getValue()))
                .collect(toList());
    }

    public List<Region> getRegions() {
        return regions;
    }

    static LocalizedTimeZone timeZone(@Nonnull ZoneId zoneId, @Nonnull Locale locale, @Nonnull String label,
                                      @Nonnull String id) {
        return new LocalizedTimeZone(id, label, zoneId.getDisplayName(TextStyle.FULL, locale),
                LocalDateTime.now().atZone(zoneId).getOffset().toString());
    }

    private static LocalizedTimeZone timeZone(@Nonnull ZoneId zoneId, @Nonnull Locale locale, @Nonnull String label) {
        return timeZone(zoneId, locale, label, zoneId.getId());
    }

    private static LocalizedTimeZone timeZone(@Nonnull ZoneId zoneId, @Nonnull Locale locale) {
        return timeZone(zoneId, locale, zoneId.getId());
    }

    /**
     * @param zoneId the zone id to parse the region for
     * @return a pair containing the region and label for the zoneId or {@link Optional#empty()} if the zoneId does not
     * have a region
     */
    private static Optional<Pair<String, String>> getRegion(ZoneId zoneId) {
        int lastSlash = zoneId.getId().lastIndexOf('/');
        String id = zoneId.getId();
        if (lastSlash > 0) {
            return Optional.of(Pair.pair(toLabel(id.substring(0, lastSlash)), toLabel(id.substring(lastSlash + 1))));
        }

        return Optional.empty();
    }

    private static String toLabel(String value) {
        return value.replaceAll("_", " ");
    }

    @SuppressWarnings("WeakerAccess") // Soy needs access to this class
    public static class Region implements Comparable<Region> {

        private final String name;
        private final SortedSet<LocalizedTimeZone> timeZones;

        private Region(@Nonnull String name, @Nonnull Collection<LocalizedTimeZone> timeZones) {
            this.name = name;
            this.timeZones = ImmutableSortedSet.copyOf(timeZones);
        }

        public String getName() {
            return name;
        }

        public Set<LocalizedTimeZone> getTimeZones() {
            return timeZones;
        }

        @Override
        public int compareTo(@Nonnull Region region) {
            return this.name.compareTo(region.getName());
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }

            if (obj instanceof Region) {
                Region region = Region.class.cast(obj);
                return this.name.equals(region.getName()) &&
                        this.timeZones.equals(region.getTimeZones());
            }

            return false;
        }

        @Override
        public int hashCode() {
            return 31 * name.hashCode() + 31 * timeZones.hashCode();
        }

    }

    @SuppressWarnings("WeakerAccess") // Soy needs access to this class
    public static class LocalizedTimeZone implements Comparable<LocalizedTimeZone> {

        private final String id;
        private final String label;
        private final String displayName;
        private final String offset;

        private LocalizedTimeZone(@Nonnull String id, @Nonnull String label, @Nonnull String displayName,
                          @Nonnull String offset) {
            this.id = requireNonNull(id, "id");
            this.label = requireNonNull(label, "label");
            this.displayName = requireNonNull(displayName, "displayName");
            this.offset = requireNonNull(offset, "offset");
        }

        public String getId() {
            return id;
        }

        public String getLabel() {
            return label;
        }

        public String getDisplayName() {
            return displayName;
        }

        public String getOffset() {
            return offset;
        }

        @Override
        public int compareTo(@Nonnull LocalizedTimeZone timeZone) {
            return this.label.compareTo(timeZone.getLabel());
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }

            if (obj instanceof LocalizedTimeZone) {
                LocalizedTimeZone tz = LocalizedTimeZone.class.cast(obj);
                return this.id.equals(tz.getId()) &&
                        this.label.equals(tz.getLabel()) &&
                        this.displayName.equals(tz.getDisplayName()) &&
                        this.offset.equals(tz.getOffset());
            }

            return false;
        }

        @Override
        public int hashCode() {
            return 31 * id.hashCode()
                    + 31 * label.hashCode()
                    + 31 * displayName.hashCode()
                    + 31 * offset.hashCode();
        }
    }
}
