/*
 * Java
 *
 * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
 * Copyright 2024 MicroEJ Corp. This file has been modified and/or created by MicroEJ Corp.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package java.time.zone;

import java.io.IOException;
import java.time.DayOfWeek;
import java.time.LocalTime;
import java.time.Month;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;

import ej.annotation.Nullable;
import ej.bon.ResourceBuffer;

/**
 * Provides zone rules for the time-zones defined by IANA Time Zone Database (TZDB).
 */
public class TzdbZoneRulesProvider extends ZoneRulesProvider {

	private static final int MAX_RULES = 16;
	private static final int MAX_LENGTH = 1024;
	private static final int INT_SIZE = 4;
	private static final int FORMAT_VERSION = 1;
	private static final String TZDB_RESOURCE_PATH = "/java/time/zone/tzdb";
	/**
	 * The zero-length long array.
	 */
	private static final long[] EMPTY_LONG_ARRAY = new long[0];
	/**
	 * The zero-length lastrules array.
	 */
	private static final ZoneOffsetTransitionRule[] EMPTY_LASTRULES = new ZoneOffsetTransitionRule[0];

	private final ResourceBuffer buffer;
	private final int zonesCount;
	/** Offset in the resource buffer at which the array of zone ids begins. */
	private final int zoneIdsOffset;
	/** Offset in the resource buffer at which the array of zone ids offsets begins. */
	private final int zoneIdsOffsets;
	/** Offset in the resource buffer at which the array of zone rules offsets begins. */
	private final int zoneRulesOffsets;

	/**
	 * Creates a provider and initializes it.
	 * <p>
	 * The initialization phase attempts to read from the tzdb binary resource that contains the zone rules. If it fails
	 * to open or read the resource, it will throw an exception.
	 *
	 * @throws IOException
	 *             if the tzdb binary resource could not be open/read, or the tzdb data is not valid
	 */
	public TzdbZoneRulesProvider() throws IOException {
		ResourceBuffer resourceBuffer;
		try {
			resourceBuffer = new ResourceBuffer(TZDB_RESOURCE_PATH);
		} catch (IOException e) {
			throw new IOException("Cannot open the tzdb binary resource");
		}
		final int size = resourceBuffer.available();

		checkHeader(resourceBuffer);
		// read version
		resourceBuffer.readString();
		int nbZones = resourceBuffer.readShort() & 0xFFFF;
		if (nbZones > MAX_LENGTH) {
			// defensive check to avoid OOM from corrupted/invalid data
			throw new IOException("Too many zones");
		}

		this.zoneIdsOffset = size - resourceBuffer.available();
		int offsetsLength = nbZones * INT_SIZE;
		this.zoneIdsOffsets = size - 2 * offsetsLength;
		this.zoneRulesOffsets = size - offsetsLength;
		this.zonesCount = nbZones;
		this.buffer = resourceBuffer;
	}

	private static void checkHeader(ResourceBuffer buffer) throws IOException {
		// check signature and version
		if (buffer.readByte() != (byte) 'M' || buffer.readByte() != (byte) 'E' || buffer.readByte() != (byte) 'J'
				|| buffer.readByte() != (byte) '_' || buffer.readByte() != (byte) 'T' || buffer.readByte() != (byte) 'Z'
				|| buffer.readByte() != (byte) 'D') {
			throw new IOException("Wrong tzdb header");
		}
		byte readFormat = buffer.readByte();
		if (readFormat != FORMAT_VERSION) {
			throw new IOException(
					"Wrong tzdb resource format version, read " + readFormat + ", expected " + FORMAT_VERSION);
		}
	}

	@Override
	protected Set<String> provideZoneIds() {
		ResourceBuffer resourceBuffer = this.buffer;
		synchronized (resourceBuffer) {
			try {
				seek(resourceBuffer, this.zoneIdsOffset);
				Set<String> zoneIds = new HashSet<>();
				for (int i = 0; i < this.zonesCount; i++) {
					zoneIds.add(resourceBuffer.readString());
				}
				return zoneIds;
			} catch (IOException e) {
				throw new ZoneRulesException("Error when reading the region ids", e);
			}
		}
	}

	@Override
	@Nullable
	protected ZoneRules provideRules(String zoneId, boolean forCaching) {
		ResourceBuffer resourceBuffer = this.buffer;
		synchronized (resourceBuffer) {
			try {
				int ruleIndex = binarySearch(resourceBuffer, zoneId);
				if (ruleIndex > -1) {
					// get the rule offset from the array of offsets
					seek(resourceBuffer, this.zoneRulesOffsets + ruleIndex * INT_SIZE);
					int ruleOffset = resourceBuffer.readInt();
					// read the zone rules
					seek(resourceBuffer, ruleOffset);
					return readZoneRules(resourceBuffer);
				}
				throw new ZoneRulesException("No rule found for zone id " + zoneId);
			} catch (IOException e) {
				throw new ZoneRulesException("Error when reading the region rules", e);
			}
		}
	}

	private static void seek(ResourceBuffer buffer, int offset) throws IOException {
		try {
			buffer.seek(offset);
		} catch (IndexOutOfBoundsException e) {
			throw new IOException(e);
		}
	}

	private int binarySearch(ResourceBuffer resourceBuffer, String zoneId) throws IOException {
		int low = 0;
		int high = this.zonesCount - 1;

		while (low <= high) {
			int mid = (low + high) >>> 1;
			// get the id offset from the array of offsets
			seek(resourceBuffer, this.zoneIdsOffsets + mid * INT_SIZE);
			int offset = resourceBuffer.readInt();
			// read the zone id
			seek(resourceBuffer, offset);
			String id = resourceBuffer.readString();

			int compare = id.compareTo(zoneId);
			if (compare < 0) {
				low = mid + 1;
			} else if (compare > 0) {
				high = mid - 1;
			} else {
				return mid;
			}
		}
		return -1;
	}

	@Override
	protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) {
		throw new ZoneRulesException("History not available"); //$NON-NLS-1$
	}

	private static ZoneRules readZoneRules(ResourceBuffer buffer) throws IOException {
		Map<Integer, ZoneOffset> offsets = new HashMap<>(0);
		int stdSize = buffer.readShort() & 0xFFFF;
		if (stdSize > MAX_LENGTH) {
			// defensive check to avoid OOM from corrupted/invalid data
			throw new IOException("Too many transitions");
		}
		long[] stdTrans = (stdSize == 0) ? EMPTY_LONG_ARRAY : new long[stdSize];
		for (int i = 0; i < stdSize; i++) {
			stdTrans[i] = readEpochSec(buffer);
		}
		ZoneOffset[] stdOffsets = new ZoneOffset[stdSize + 1];
		for (int i = 0; i < stdOffsets.length; i++) {
			stdOffsets[i] = readOffset(buffer, offsets);
		}
		int savSize = buffer.readShort() & 0xFFFF;
		if (savSize > MAX_LENGTH) {
			// defensive check to avoid OOM from corrupted/invalid data
			throw new IOException("Too many saving offsets");
		}
		long[] savTrans = (savSize == 0) ? EMPTY_LONG_ARRAY : new long[savSize];
		for (int i = 0; i < savSize; i++) {
			savTrans[i] = readEpochSec(buffer);
		}
		ZoneOffset[] savOffsets = new ZoneOffset[savSize + 1];
		for (int i = 0; i < savOffsets.length; i++) {
			savOffsets[i] = readOffset(buffer, offsets);
		}
		int ruleSize = buffer.readByte() & 0xFF;
		if (ruleSize > MAX_RULES) {
			// defensive check to avoid OOM from corrupted/invalid data
			throw new IOException("Too many transition rules");
		}
		ZoneOffsetTransitionRule[] rules = (ruleSize == 0) ? EMPTY_LASTRULES : new ZoneOffsetTransitionRule[ruleSize];
		for (int i = 0; i < ruleSize; i++) {
			rules[i] = readZoneOffsetTransitionRule(buffer, offsets);
		}
		return new ZoneRules(stdTrans, stdOffsets, savTrans, savOffsets, rules);
	}

	private static ZoneOffset readOffset(ResourceBuffer buffer, Map<Integer, ZoneOffset> offsets) throws IOException {
		int offsetByte = buffer.readByte();
		return readOffset(buffer, offsetByte, 127, offsetByte * 900, offsets);
	}

	private static ZoneOffset readOffset(ResourceBuffer buffer, int readByte, int testValue, int seconds,
			Map<Integer, ZoneOffset> offsets) throws IOException {
		int totalSeconds = (readByte == testValue ? buffer.readInt() : seconds);
		Integer key = Integer.valueOf(totalSeconds);
		ZoneOffset zoneOffset = offsets.get(key);
		if (zoneOffset != null) {
			return zoneOffset;
		}
		zoneOffset = ZoneOffset.ofTotalSeconds(totalSeconds);
		offsets.put(key, zoneOffset);
		return zoneOffset;
	}

	private static long readEpochSec(ResourceBuffer buffer) throws IOException {
		int hiByte = buffer.readByte() & 255;
		if (hiByte == 255) {
			long hiInt = buffer.readInt();
			long loInt = buffer.readInt();
			return (hiInt << 32) | (loInt & 0xFFFFFFFFL);
		} else {
			int midByte = buffer.readByte() & 255;
			int loByte = buffer.readByte() & 255;
			long tot = ((hiByte << 16) + (midByte << 8) + loByte);
			return (tot * 900) - 4575744000L;
		}
	}

	private static ZoneOffsetTransitionRule readZoneOffsetTransitionRule(ResourceBuffer buffer,
			Map<Integer, ZoneOffset> offsets) throws IOException {
		int data = buffer.readInt();
		// Field description:
		// Month [31-28]
		// Day of month bytes [27-22]
		// Day of week bytes [21-19]
		// Time of transition [18-14]
		// Time definition [13-12]
		// Standard offset [11-4]
		// Offset before transition [3-2]
		// Offset after transition [1-0]
		Month month = Month.of(data >>> 28);
		int dom = ((data & (63 << 22)) >>> 22) - 32;
		int dowByte = (data & (7 << 19)) >>> 19;
		DayOfWeek dow = dowByte == 0 ? null : DayOfWeek.of(dowByte);
		int timeByte = (data & (31 << 14)) >>> 14;
		ZoneOffsetTransitionRule.TimeDefinition defn = ZoneOffsetTransitionRule.TimeDefinition
				.values()[(data & (3 << 12)) >>> 12];
		int stdByte = (data & (255 << 4)) >>> 4;
		int beforeByte = (data & (3 << 2)) >>> 2;
		int afterByte = (data & 3);
		LocalTime time = (timeByte == 31 ? LocalTime.ofSecondOfDay(buffer.readInt()) : LocalTime.of(timeByte % 24, 0));
		ZoneOffset std = readOffset(buffer, stdByte, 255, (stdByte - 128) * 900, offsets);
		ZoneOffset before = readOffset(buffer, beforeByte, 3, std.getTotalSeconds() + beforeByte * 1800, offsets);
		ZoneOffset after = readOffset(buffer, afterByte, 3, std.getTotalSeconds() + afterByte * 1800, offsets);
		return ZoneOffsetTransitionRule.of(month, dom, dow, time, timeByte == 24, defn, std, before, after);
	}
}